Update app.py
Browse files
app.py
CHANGED
|
@@ -3,12 +3,22 @@ import streamlit as st
|
|
| 3 |
import random
|
| 4 |
import time
|
| 5 |
import math
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
# --- 페이지 설정 (스크립트 최상단) ---
|
| 8 |
st.set_page_config(layout="wide", page_title="진실을 찾아서: 현대사 취재기록")
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
# --- 게임 설정 ---
|
| 11 |
-
ACTIONS_PER_TURN_LIMIT = 2
|
| 12 |
|
| 13 |
# --- 시나리오 설정 ---
|
| 14 |
SCENARIOS = {
|
|
@@ -17,37 +27,34 @@ SCENARIOS = {
|
|
| 17 |
"start_year": 1960,
|
| 18 |
"player_role": "신입 기자 (자유일보)",
|
| 19 |
"initial_press_freedom": 40,
|
| 20 |
-
"initial_scoop_points": 0,
|
| 21 |
"initial_reporter_safety": 70,
|
| 22 |
"initial_public_trust": 50,
|
| 23 |
"vocab_level": "보통",
|
| 24 |
-
"max_turns": 8,
|
| 25 |
},
|
| 26 |
"5.18_gwangju": {
|
| 27 |
"display_name": "5.18 광주 현장 취재 (1980)",
|
| 28 |
"start_year": 1980,
|
| 29 |
"player_role": "지방 주재 기자 (민주신문)",
|
| 30 |
"initial_press_freedom": 20,
|
| 31 |
-
"initial_scoop_points": 0,
|
| 32 |
"initial_reporter_safety": 50,
|
| 33 |
"initial_public_trust": 40,
|
| 34 |
"vocab_level": "보통",
|
| 35 |
-
"max_turns":
|
| 36 |
},
|
| 37 |
"june_struggle": {
|
| 38 |
"display_name": "6월 항쟁 동행 취재 (1987)",
|
| 39 |
"start_year": 1987,
|
| 40 |
"player_role": "사회부 기자 (시민일보)",
|
| 41 |
"initial_press_freedom": 30,
|
| 42 |
-
"initial_scoop_points": 0,
|
| 43 |
"initial_reporter_safety": 60,
|
| 44 |
"initial_public_trust": 45,
|
| 45 |
"vocab_level": "보통",
|
| 46 |
-
"max_turns": 7,
|
| 47 |
}
|
| 48 |
}
|
| 49 |
|
| 50 |
-
# --- 텍스트 저장소
|
| 51 |
ALL_TEXTS = {
|
| 52 |
# --- 공통 UI ---
|
| 53 |
"game_title": {"보통": "🎙️ 진실을 찾아서: 현대사 취재기록"},
|
|
@@ -56,7 +63,7 @@ ALL_TEXTS = {
|
|
| 56 |
"dashboard_title": {"보통": "📊 기자 상황판"},
|
| 57 |
"dashboard_term": {"보통": "{year}년 {turn}번째 취재일"},
|
| 58 |
"term_press_freedom": {"보통": "취재 자유도"},
|
| 59 |
-
"
|
| 60 |
"term_reporter_safety": {"보통": "기자 안전도"},
|
| 61 |
"term_public_trust": {"보통": "대중 신뢰도"},
|
| 62 |
"current_assignment_title": {"보통": "📋 오늘의 취재 지시"},
|
|
@@ -72,84 +79,142 @@ ALL_TEXTS = {
|
|
| 72 |
"historical_source_title": {"보통": "참고 자료"},
|
| 73 |
"reporter_notebook_title": {"보통": "📝 나의 취재 노트"},
|
| 74 |
"article_writing_title": {"보통": "🖋️ 기사 작성"},
|
| 75 |
-
"desk_feedback_title": {"보통": "📢
|
| 76 |
"status_loading_assignment": {"보통": "{year}년 {turn}번째 취재일 준비 중..."},
|
| 77 |
"status_actions_taken": {"보통": "오늘의 주요 취재 활동을 마쳤습니다. 기사를 정리하거나 다음 날로 진행하세요."},
|
| 78 |
"sidebar_title": {"보통": "메뉴"},
|
| 79 |
"sidebar_glossary_title": {"보통": "📰 관련 용어/인물"},
|
| 80 |
"sidebar_current_source_title": {"보통": "📎 현재 참고 자료"},
|
| 81 |
"sidebar_no_source": {"보통": "현재 참고할 만한 특별 자료가 없습니다."},
|
| 82 |
-
"
|
| 83 |
-
"input_placeholder_sentence": {"보통": "간단한 문장으로 요약..."},
|
| 84 |
-
"button_add_to_notebook": {"보통": "노트에 추가"},
|
| 85 |
-
"button_submit_article": {"보통": "기사 송고"},
|
| 86 |
"article_headline_label": {"보통": "기사 제목:"},
|
| 87 |
-
"article_body_label": {"보통": "기사 본문 핵심:"},
|
| 88 |
"article_tone_label": {"보통": "기사 논조 선택:"},
|
| 89 |
"warning_empty_article": {"보통": "기사 제목과 핵심 내용을 모두 작성해주세요."},
|
| 90 |
"info_no_special_info": {"보통": "특별한 정보는 얻지 못했습니다."},
|
| 91 |
"log_freedom_loss": {"보통": " - 취재 중 제약 발생, 취재 자유도 {loss} 감소."},
|
| 92 |
"log_safety_loss": {"보통": " - 취재 중 신변 위협 감지, 기자 안전도 {loss} 감소."},
|
| 93 |
-
"log_scoop_gain": {"보통": " - 결정적 단서 포착! 특종 점수 {gain} 획득."},
|
| 94 |
"log_info_acquired": {"보통": " - 정보 획득: {info}"},
|
| 95 |
"log_no_info_acquired": {"보통": " - 특별한 정보는 얻지 못함."},
|
| 96 |
-
"log_article_submitted": {"보통": "기사 송고: '{headline}' (논조: {tone})"},
|
| 97 |
-
"log_desk_feedback": {"보통": " -
|
| 98 |
"log_trust_change": {"보통": " - 대중 신뢰도 {change:+} 변동."},
|
| 99 |
"log_freedom_change_article": {"보통": " - 취재 자유도 {change:+} 변동."},
|
| 100 |
"log_safety_change_article": {"보통": " - 기자 안전도 {change:+} 변동."},
|
| 101 |
"log_assignment_over": {"보통": "--- {scenario_name} 취재 기간 종료 ---"},
|
| 102 |
"log_next_day_start": {"보통": "--- {year}년 {turn}번째 취재일 시작 ---"},
|
| 103 |
"button_go_to_article_writing": {"보통": "기사 작성하기"},
|
|
|
|
| 104 |
|
| 105 |
# --- 4.19 혁명 취재 시나리오 ---
|
| 106 |
"scenario_419_revolution_name": {"보통": "4.19 혁명 취재 (1960)"},
|
| 107 |
-
|
| 108 |
-
"
|
| 109 |
-
"
|
| 110 |
-
"
|
| 111 |
-
"
|
| 112 |
-
"
|
| 113 |
-
"
|
| 114 |
-
"
|
| 115 |
-
"
|
| 116 |
-
|
| 117 |
-
"
|
| 118 |
-
"
|
| 119 |
-
|
| 120 |
-
"
|
| 121 |
-
"
|
| 122 |
-
"
|
| 123 |
-
"
|
| 124 |
-
"
|
| 125 |
-
|
| 126 |
-
"
|
| 127 |
-
"
|
| 128 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
# --- 5.18 광주 취재 시나리오 ---
|
| 131 |
"scenario_518_gwangju_name": {"보통": "5.18 광주 현장 취재 (1980)"},
|
| 132 |
-
|
| 133 |
-
"
|
| 134 |
-
"
|
| 135 |
-
"
|
| 136 |
-
"
|
| 137 |
-
"
|
| 138 |
-
"
|
| 139 |
-
"
|
| 140 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
# --- 6월 항쟁 취재 시나리오 ---
|
| 143 |
"scenario_june_struggle_name": {"보통": "6월 항쟁 동행 취재 (1987)"},
|
| 144 |
-
|
| 145 |
-
"
|
| 146 |
-
"
|
| 147 |
-
"
|
| 148 |
-
"
|
| 149 |
-
"
|
| 150 |
-
"
|
| 151 |
-
"
|
| 152 |
-
"
|
|
|
|
| 153 |
|
| 154 |
"article_tone_fact_based": {"보통": "객관적 사실 전달 위주"},
|
| 155 |
"article_tone_critical": {"보통": "정부/기관 비판적 논조"},
|
|
@@ -159,8 +224,23 @@ ALL_TEXTS = {
|
|
| 159 |
"glossary_419_revolution_free_party": {"보통": "**자유당 (自由黨):** 1951년 이승만 대통령을 중심으로 창당된 정당. 1960년 4.19 혁명으로 이승만 대통령이 하야하면서 사실상 해체되었다."},
|
| 160 |
"glossary_419_revolution_315_election": {"보통": "**3.15 부정선거:** 1960년 3월 15일 실시된 제4대 대통령 및 제5대 부통령 선거에서 자유당 정권이 이승만-이기붕 후보를 당선시키기 위해 자행한 대규모 부정선거. 4.19 혁명의 직접적인 도화선이 되었다."},
|
| 161 |
"glossary_419_revolution_kimjuyeol": {"보통": "**김주열 (金朱烈):** 1943~1960. 마산상업고등학교 학생. 3.15 마산 시위 중 실종되었다가 4월 11일 눈에 최루탄이 박힌 시신으로 발견되어 4.19 혁명을 격화시키는 계기가 되었다."},
|
|
|
|
|
|
|
|
|
|
| 162 |
"glossary_518_gwangju_new_military": {"보통": "**신군부 (新軍部):** 1979년 10.26 사건 이후 12.12 군사반란을 통해 권력을 장악한 전두환, 노태우 등 하나회 중심의 군부 세력."},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
"glossary_june_struggle_parkjongchul": {"보통": "**박종철 (朴鍾哲):** 1965~1987. 서울대학교 언어학과 학생. 1987년 1월 경찰의 고문으로 사망하여 6월 민주 항쟁의 도화선이 되었다."},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
}
|
| 165 |
|
| 166 |
# --- 어휘 조정 함수 ---
|
|
@@ -187,7 +267,8 @@ def initialize_reporter_scenario_state(scenario_key):
|
|
| 187 |
'game_year': scenario_settings['start_year'],
|
| 188 |
'status': {
|
| 189 |
'press_freedom': scenario_settings['initial_press_freedom'],
|
| 190 |
-
'
|
|
|
|
| 191 |
'reporter_safety': scenario_settings['initial_reporter_safety'],
|
| 192 |
'public_trust': scenario_settings['initial_public_trust'],
|
| 193 |
},
|
|
@@ -201,39 +282,90 @@ HISTORICAL_ASSIGNMENTS = {
|
|
| 201 |
"4.19_revolution": [
|
| 202 |
{"turn": 1, "assignment_key": "event_419_t1_assignment", "source_key": "event_419_t1_source",
|
| 203 |
"options": [
|
| 204 |
-
{"action_key": "action_419_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 20, "
|
| 205 |
-
{"action_key": "action_419_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 5, "
|
| 206 |
-
{"action_key": "action_419_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 10, "
|
| 207 |
-
], "article_writing_phase": True,
|
| 208 |
-
"desk_feedback_keys": {"fact_based": "desk_feedback_419_t1_fact_based", "critical": "desk_feedback_419_t1_critical", "cautious": "desk_feedback_419_t1_cautious", "sympathetic": "desk_feedback_419_t1_sympathetic"}},
|
| 209 |
{"turn": 2, "assignment_key": "event_419_t2_assignment", "source_key": "event_419_t2_source",
|
| 210 |
"options": [
|
| 211 |
-
{"action_key": "action_419_t2_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "
|
| 212 |
-
{"action_key": "action_419_t2_opt2_text", "cost_freedom_risk": 10, "safety_risk": 15, "
|
| 213 |
-
{"action_key": "action_419_t2_opt3_text", "cost_freedom_risk": 20, "safety_risk": 10, "
|
| 214 |
-
], "article_writing_phase": True,
|
| 215 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
],
|
| 217 |
"5.18_gwangju": [
|
| 218 |
{"turn": 1, "assignment_key": "event_518_t1_assignment", "source_key": "event_518_t1_source",
|
| 219 |
"options": [
|
| 220 |
-
{"action_key": "action_518_t1_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "
|
| 221 |
-
{"action_key": "action_518_t1_opt2_text", "cost_freedom_risk": 10, "safety_risk": 20, "
|
| 222 |
-
{"action_key": "action_518_t1_opt3_text", "cost_freedom_risk": 20, "safety_risk": 30, "
|
| 223 |
-
], "article_writing_phase": False,
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
],
|
| 226 |
"june_struggle": [
|
| 227 |
{"turn": 1, "assignment_key": "event_june_t1_assignment", "source_key": "event_june_t1_source",
|
| 228 |
"options": [
|
| 229 |
-
{"action_key": "action_june_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 15, "
|
| 230 |
-
{"action_key": "action_june_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 10, "
|
| 231 |
-
{"action_key": "action_june_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "
|
| 232 |
-
], "article_writing_phase": True,
|
| 233 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
]
|
| 235 |
}
|
| 236 |
-
|
| 237 |
# --- 다음 취재 지시 가져오기 ---
|
| 238 |
def get_next_assignment(scenario_key, current_turn, game_state):
|
| 239 |
scenario_assignments = HISTORICAL_ASSIGNMENTS.get(scenario_key, [])
|
|
@@ -247,14 +379,13 @@ def get_next_assignment(scenario_key, current_turn, game_state):
|
|
| 247 |
"source_text": get_text_for_assignment(assignment_data.get("source_key", "")),
|
| 248 |
"options": [],
|
| 249 |
"article_writing_phase": assignment_data.get("article_writing_phase", False),
|
| 250 |
-
"
|
| 251 |
}
|
| 252 |
for opt_data in assignment_data.get("options", []):
|
| 253 |
final_assignment["options"].append({
|
| 254 |
"action_text": get_text_for_assignment(opt_data["action_key"]),
|
| 255 |
"cost_freedom_risk": opt_data.get("cost_freedom_risk", 0),
|
| 256 |
"safety_risk": opt_data.get("safety_risk", 0),
|
| 257 |
-
"scoop_potential": opt_data.get("scoop_potential", 0),
|
| 258 |
"info_key": opt_data.get("info_key", "")
|
| 259 |
})
|
| 260 |
|
|
@@ -280,12 +411,6 @@ def process_reporter_action(selected_option, game_state):
|
|
| 280 |
status['reporter_safety'] -= safety_loss
|
| 281 |
game_state['event_log'].append(get_text_for_processing("log_safety_loss").format(loss=safety_loss))
|
| 282 |
|
| 283 |
-
scoop_gain_potential = selected_option.get("scoop_potential", 0)
|
| 284 |
-
if scoop_gain_potential > 0 and random.random() < 0.7:
|
| 285 |
-
actual_scoop = random.randint(scoop_gain_potential // 2, scoop_gain_potential)
|
| 286 |
-
status['scoop_points'] += actual_scoop
|
| 287 |
-
game_state['event_log'].append(get_text_for_processing("log_scoop_gain").format(gain=actual_scoop))
|
| 288 |
-
|
| 289 |
info_text = ""
|
| 290 |
if selected_option.get("info_key"):
|
| 291 |
info_text = get_text_for_processing(selected_option["info_key"])
|
|
@@ -312,12 +437,12 @@ def generate_article_interface(game_state, vocab_level):
|
|
| 312 |
for idx, note in enumerate(game_state['reporter_notebook']):
|
| 313 |
st.markdown(f"- {note}")
|
| 314 |
else:
|
| 315 |
-
st.caption("아직 취재한 내용이 없습니다.")
|
| 316 |
st.markdown("---")
|
| 317 |
|
| 318 |
with st.form(key="article_form"):
|
| 319 |
-
article_headline = st.text_input(get_text_func("article_headline_label"), placeholder=
|
| 320 |
-
article_body_summary = st.text_area(get_text_func("article_body_label"), height=150, placeholder=
|
| 321 |
|
| 322 |
article_tones = {
|
| 323 |
"fact_based": get_text_func("article_tone_fact_based"),
|
|
@@ -338,6 +463,10 @@ def generate_article_interface(game_state, vocab_level):
|
|
| 338 |
st.warning(get_text_func("warning_empty_article"))
|
| 339 |
return None
|
| 340 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
return {
|
| 342 |
"turn": game_state['current_turn'],
|
| 343 |
"headline": article_headline,
|
|
@@ -347,68 +476,93 @@ def generate_article_interface(game_state, vocab_level):
|
|
| 347 |
}
|
| 348 |
return None
|
| 349 |
|
| 350 |
-
# --- 기사 평가 및 데스크 피드백 ---
|
| 351 |
-
def
|
| 352 |
status = game_state['status']
|
| 353 |
vocab_level = SCENARIOS[game_state['scenario_key']]["vocab_level"]
|
| 354 |
get_text_func = lambda k: get_text(k, vocab_level)
|
| 355 |
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
|
|
|
|
| 395 |
status['public_trust'] = max(0, min(100, status['public_trust'] + trust_change))
|
| 396 |
-
status['press_freedom'] = max(0, min(100, status['press_freedom'] + freedom_change))
|
| 397 |
-
status['reporter_safety'] = max(0, min(100, status['reporter_safety'] + safety_change))
|
| 398 |
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
-
|
|
|
|
|
|
|
| 404 |
if trust_change != 0: game_state['event_log'].append(get_text_func("log_trust_change").format(change=trust_change))
|
| 405 |
-
if freedom_change != 0: game_state['event_log'].append(get_text_func("log_freedom_change_article").format(change=freedom_change))
|
| 406 |
-
if safety_change != 0: game_state['event_log'].append(get_text_func("log_safety_change_article").format(change=safety_change))
|
| 407 |
|
| 408 |
game_state['submitted_articles'].append(article)
|
| 409 |
game_state['reporter_notebook'] = []
|
| 410 |
|
| 411 |
-
return
|
| 412 |
|
| 413 |
# --- UI 표시 함수들 ---
|
| 414 |
def display_reporter_dashboard(game_state):
|
|
@@ -417,11 +571,14 @@ def display_reporter_dashboard(game_state):
|
|
| 417 |
get_text_for_ui = lambda k: get_text(k, vocab_level)
|
| 418 |
status = game_state['status']
|
| 419 |
|
|
|
|
|
|
|
| 420 |
col1, col2, col3, col4 = st.columns(4)
|
| 421 |
with col1: st.metric(get_text_for_ui("term_public_trust"), f"{status['public_trust']}%")
|
| 422 |
with col2: st.metric(get_text_for_ui("term_press_freedom"), f"{status['press_freedom']}%")
|
| 423 |
with col3: st.metric(get_text_for_ui("term_reporter_safety"), f"{status['reporter_safety']}%")
|
| 424 |
-
with col4: st.metric(get_text_for_ui("
|
|
|
|
| 425 |
|
| 426 |
def display_reporter_notebook(game_state, vocab_level):
|
| 427 |
get_text_func = lambda k: get_text(k, vocab_level)
|
|
@@ -505,9 +662,9 @@ def reporter_simulation_main():
|
|
| 505 |
st.subheader("송고한 주요 기사 목록")
|
| 506 |
if game_state['submitted_articles']:
|
| 507 |
for article in game_state['submitted_articles']:
|
| 508 |
-
tone_text_key = f"article_tone_{article['tone']}"
|
| 509 |
-
tone_display_text = get_text_main(tone_text_key)
|
| 510 |
-
st.markdown(f"- **{article['headline']}** (논조: {tone_display_text}) - {article['turn']}일차 송고")
|
| 511 |
else:
|
| 512 |
st.caption("이번 취재 기간 동안 송고한 기사가 없습니다.")
|
| 513 |
|
|
@@ -556,7 +713,7 @@ def reporter_simulation_main():
|
|
| 556 |
st.session_state.desk_feedback_message = None
|
| 557 |
st.session_state.game_mode = 'reporter_action'
|
| 558 |
st.rerun()
|
| 559 |
-
else:
|
| 560 |
st.session_state.game_mode = 'assignment_over'
|
| 561 |
if game_state.get('event_log') is not None:
|
| 562 |
game_state['event_log'].append(get_text_main("log_assignment_over").format(scenario_name=SCENARIOS[scenario_key]['display_name']))
|
|
@@ -594,25 +751,25 @@ def reporter_simulation_main():
|
|
| 594 |
assignment_data = st.session_state.current_assignment_data
|
| 595 |
submitted_article = generate_article_interface(game_state, vocab_level)
|
| 596 |
if submitted_article:
|
| 597 |
-
feedback =
|
| 598 |
st.session_state.desk_feedback_message = feedback
|
| 599 |
-
st.session_state.game_mode = 'reporter_action'
|
| 600 |
st.rerun()
|
| 601 |
|
| 602 |
if st.session_state.desk_feedback_message and st.session_state.game_mode == 'reporter_action':
|
| 603 |
st.divider()
|
| 604 |
st.subheader(get_text_main("desk_feedback_title"))
|
| 605 |
-
st.warning(st.session_state.desk_feedback_message)
|
| 606 |
|
| 607 |
-
with col_actions_notebook:
|
| 608 |
st.header("활동 기록 및 다음 단계")
|
| 609 |
can_proceed_to_next_day = False
|
| 610 |
if st.session_state.game_mode == 'reporter_action' and st.session_state.current_assignment_data:
|
| 611 |
assignment_data = st.session_state.current_assignment_data
|
| 612 |
if st.session_state.actions_taken_this_turn >= ACTIONS_PER_TURN_LIMIT:
|
| 613 |
-
if not assignment_data.get("article_writing_phase"):
|
| 614 |
can_proceed_to_next_day = True
|
| 615 |
-
elif st.session_state.desk_feedback_message:
|
| 616 |
can_proceed_to_next_day = True
|
| 617 |
|
| 618 |
if can_proceed_to_next_day:
|
|
@@ -620,7 +777,7 @@ def reporter_simulation_main():
|
|
| 620 |
current_turn = game_state['current_turn']
|
| 621 |
max_turns_for_scenario = SCENARIOS[scenario_key]['max_turns']
|
| 622 |
|
| 623 |
-
if current_turn >= max_turns_for_scenario:
|
| 624 |
st.session_state.game_mode = 'assignment_over'
|
| 625 |
if game_state.get('event_log') is not None:
|
| 626 |
game_state['event_log'].append(get_text_main("log_assignment_over").format(scenario_name=SCENARIOS[scenario_key]['display_name']))
|
|
@@ -632,6 +789,7 @@ def reporter_simulation_main():
|
|
| 632 |
|
| 633 |
st.session_state.current_assignment_data = None
|
| 634 |
st.session_state.actions_taken_this_turn = 0
|
|
|
|
| 635 |
st.rerun()
|
| 636 |
|
| 637 |
st.divider()
|
|
@@ -663,7 +821,7 @@ def reporter_simulation_main():
|
|
| 663 |
keys_to_clear = ['current_scenario_key', 'game_state', 'current_assignment_data', 'actions_taken_this_turn', 'desk_feedback_message']
|
| 664 |
for k_to_clear in keys_to_clear:
|
| 665 |
if k_to_clear in st.session_state: del st.session_state[k_to_clear]
|
| 666 |
-
time.sleep(1)
|
| 667 |
st.rerun()
|
| 668 |
|
| 669 |
if __name__ == "__main__":
|
|
|
|
| 3 |
import random
|
| 4 |
import time
|
| 5 |
import math
|
| 6 |
+
import os
|
| 7 |
+
from openai import OpenAI
|
| 8 |
+
import json
|
| 9 |
|
| 10 |
# --- 페이지 설정 (스크립트 최상단) ---
|
| 11 |
st.set_page_config(layout="wide", page_title="진실을 찾아서: 현대사 취재기록")
|
| 12 |
|
| 13 |
+
# --- OpenAI API 키 설정 ---
|
| 14 |
+
if "OPENAI_API_KEY" not in os.environ:
|
| 15 |
+
st.error("OPENAI_API_KEY 환경 변수가 설정되지 않았습니다. API 키를 설정해주세요.")
|
| 16 |
+
st.stop()
|
| 17 |
+
|
| 18 |
+
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
|
| 19 |
+
|
| 20 |
# --- 게임 설정 ---
|
| 21 |
+
ACTIONS_PER_TURN_LIMIT = 2
|
| 22 |
|
| 23 |
# --- 시나리오 설정 ---
|
| 24 |
SCENARIOS = {
|
|
|
|
| 27 |
"start_year": 1960,
|
| 28 |
"player_role": "신입 기자 (자유일보)",
|
| 29 |
"initial_press_freedom": 40,
|
|
|
|
| 30 |
"initial_reporter_safety": 70,
|
| 31 |
"initial_public_trust": 50,
|
| 32 |
"vocab_level": "보통",
|
| 33 |
+
"max_turns": 8,
|
| 34 |
},
|
| 35 |
"5.18_gwangju": {
|
| 36 |
"display_name": "5.18 광주 현장 취재 (1980)",
|
| 37 |
"start_year": 1980,
|
| 38 |
"player_role": "지방 주재 기자 (민주신문)",
|
| 39 |
"initial_press_freedom": 20,
|
|
|
|
| 40 |
"initial_reporter_safety": 50,
|
| 41 |
"initial_public_trust": 40,
|
| 42 |
"vocab_level": "보통",
|
| 43 |
+
"max_turns": 8,
|
| 44 |
},
|
| 45 |
"june_struggle": {
|
| 46 |
"display_name": "6월 항쟁 동행 취재 (1987)",
|
| 47 |
"start_year": 1987,
|
| 48 |
"player_role": "사회부 기자 (시민일보)",
|
| 49 |
"initial_press_freedom": 30,
|
|
|
|
| 50 |
"initial_reporter_safety": 60,
|
| 51 |
"initial_public_trust": 45,
|
| 52 |
"vocab_level": "보통",
|
| 53 |
+
"max_turns": 7,
|
| 54 |
}
|
| 55 |
}
|
| 56 |
|
| 57 |
+
# --- 텍스트 저장소 ---
|
| 58 |
ALL_TEXTS = {
|
| 59 |
# --- 공통 UI ---
|
| 60 |
"game_title": {"보통": "🎙️ 진실을 찾아서: 현대사 취재기록"},
|
|
|
|
| 63 |
"dashboard_title": {"보통": "📊 기자 상황판"},
|
| 64 |
"dashboard_term": {"보통": "{year}년 {turn}번째 취재일"},
|
| 65 |
"term_press_freedom": {"보통": "취재 자유도"},
|
| 66 |
+
"term_article_score_avg": {"보통": "평균 기사 점수"},
|
| 67 |
"term_reporter_safety": {"보통": "기자 안전도"},
|
| 68 |
"term_public_trust": {"보통": "대중 신뢰도"},
|
| 69 |
"current_assignment_title": {"보통": "📋 오늘의 취재 지시"},
|
|
|
|
| 79 |
"historical_source_title": {"보통": "참고 자료"},
|
| 80 |
"reporter_notebook_title": {"보통": "📝 나의 취재 노트"},
|
| 81 |
"article_writing_title": {"보통": "🖋️ 기사 작성"},
|
| 82 |
+
"desk_feedback_title": {"보통": "📢 AI 편집장 평가"},
|
| 83 |
"status_loading_assignment": {"보통": "{year}년 {turn}번째 취재일 준비 중..."},
|
| 84 |
"status_actions_taken": {"보통": "오늘의 주요 취재 활동을 마쳤습니다. 기사를 정리하거나 다음 날로 진행하세요."},
|
| 85 |
"sidebar_title": {"보통": "메뉴"},
|
| 86 |
"sidebar_glossary_title": {"보통": "📰 관련 용어/인물"},
|
| 87 |
"sidebar_current_source_title": {"보통": "📎 현재 참고 자료"},
|
| 88 |
"sidebar_no_source": {"보통": "현재 참고할 만한 특별 자료가 없습니다."},
|
| 89 |
+
"button_submit_article": {"보통": "기사 송고 (AI 평가)"},
|
|
|
|
|
|
|
|
|
|
| 90 |
"article_headline_label": {"보통": "기사 제목:"},
|
| 91 |
+
"article_body_label": {"보통": "기사 본문 핵심 (3-5문장 요약):"},
|
| 92 |
"article_tone_label": {"보통": "기사 논조 선택:"},
|
| 93 |
"warning_empty_article": {"보통": "기사 제목과 핵심 내용을 모두 작성해주세요."},
|
| 94 |
"info_no_special_info": {"보통": "특별한 정보는 얻지 못했습니다."},
|
| 95 |
"log_freedom_loss": {"보통": " - 취재 중 제약 발생, 취재 자유도 {loss} 감소."},
|
| 96 |
"log_safety_loss": {"보통": " - 취재 중 신변 위협 감지, 기자 안전도 {loss} 감소."},
|
|
|
|
| 97 |
"log_info_acquired": {"보통": " - 정보 획득: {info}"},
|
| 98 |
"log_no_info_acquired": {"보통": " - 특별한 정보는 얻지 못함."},
|
| 99 |
+
"log_article_submitted": {"보통": "기사 송고: '{headline}' (논조: {tone}) - AI 평가 점수: {score}점"},
|
| 100 |
+
"log_desk_feedback": {"보통": " - AI 편집장 코멘트: {feedback}"},
|
| 101 |
"log_trust_change": {"보통": " - 대중 신뢰도 {change:+} 변동."},
|
| 102 |
"log_freedom_change_article": {"보통": " - 취재 자유도 {change:+} 변동."},
|
| 103 |
"log_safety_change_article": {"보통": " - 기자 안전도 {change:+} 변동."},
|
| 104 |
"log_assignment_over": {"보통": "--- {scenario_name} 취재 기간 종료 ---"},
|
| 105 |
"log_next_day_start": {"보통": "--- {year}년 {turn}번째 취재일 시작 ---"},
|
| 106 |
"button_go_to_article_writing": {"보통": "기사 작성하기"},
|
| 107 |
+
"error_openai_api": {"보통": "OpenAI API 호출 중 오류가 발생했습니다: {error}"},
|
| 108 |
|
| 109 |
# --- 4.19 혁명 취재 시나리오 ---
|
| 110 |
"scenario_419_revolution_name": {"보통": "4.19 혁명 취재 (1960)"},
|
| 111 |
+
# Turn 1 (3.15 부정선거)
|
| 112 |
+
"event_419_t1_assignment": {"보통": "3.15 정부통령 선거일입니다. 전국 투표소 상황 및 개표 과정에서 예상되는 부정행위 정황을 포착하고, 관련자(참관인, 유권자, 선관위 관계자 등)의 증언을 확보하는 것이 오늘의 주요 임무입니다. 특히 야당 참관인들의 활동과 여당 측의 조직적인 움직임을 면밀히 관찰하십시오."},
|
| 113 |
+
"event_419_t1_source": {"보통": "[배경 정보] 현재 자유당 정권의 장기집권에 대한 국민적 불만이 높은 상황입니다. 이번 선거에서 이승만 대통령의 4선과 이기붕 부통령의 당선을 위해 정부와 여당이 조직적으로 선거에 개입할 것이라는 예측이 지배적입니다. 사전투표함 바꿔치기, 유령 유권자 동원, 3인조 및 5인조 공개투표 강요, 야당 참관인 축출 등의 소문이 이미 파다하게 퍼져 있습니다."},
|
| 114 |
+
"action_419_t1_opt1_text": {"보통": "서울 시내 ���요 투표소 잠입 및 비밀 촬영 시도"},
|
| 115 |
+
"action_419_t1_opt2_text": {"보통": "야당 선거 감시단과 동행하며 부정 사례 공동 취재"},
|
| 116 |
+
"action_419_t1_opt3_text": {"보통": "선관위 내부 고발자로 추정되는 인물과 비밀 접선 시도"},
|
| 117 |
+
"info_419_t1_opt1_got": {"보통": "정보: 종로구 A투표소에서 자유당 완장을 찬 청년들이 투표함에 다량의 투표용지를 추가로 넣는 장면을 멀리서 목격함. 접근하려 하자 위협적인 눈초리를 보냄. (키워드: 투표함 추가 투입, 자유당 완장, 위협)"},
|
| 118 |
+
"info_419_t1_opt2_got": {"보통": "정보: 민주당 참관인 박씨, '성북구 B투표소에서 경찰이 특정 유권자들에게 기표소까지 동행하며 기표 내용을 확인하는 것을 목격했다. 항의했으나 묵살당했다'고 격앙된 목소리로 증언. 관련 사진 일부 확보. (키워드: 경찰 동행 기표, 참관인 항의 묵살, 민주당)"},
|
| 119 |
+
"info_419_t1_opt3_got": {"보통": "정보: 익명을 요구한 선관위 하급 직원, '오늘 새벽, 일부 지역 투표소로 향하는 사전투표함 중 몇 개가 정체불명의 차량으로 옮겨지는 것을 보았다. 매우 불안하다'고 떨리는 목소리로 제보. (키워드: 사전투표함 교체 의혹, 선관위 내부 제보, 불안감)"},
|
| 120 |
+
# Turn 2 (김주열 열사 발견)
|
| 121 |
+
"event_419_t2_assignment": {"보통": "마산에서 3.15 부정선거에 항의하던 시위 중 실종된 김주열 학생의 시신이 참혹한 모습으로 발견되었습니다. 이 사건의 정확한 사망 원인, 경찰의 초기 대응 과정에서의 문제점, 그리고 마산 시민과 학생들의 격앙된 반응을 심층 취재하십시오."},
|
| 122 |
+
"event_419_t2_source": {"보통": "[배경 정보] 4월 11일, 마산 중앙부두 앞바다에서 오른쪽 눈에 최루탄이 박힌 채 떠오른 김주열 군의 시신은 전국민에게 엄청난 충격을 주었습니다. 경찰은 초기 사인을 단순 익사로 발표하려 했으나, 시신 상태가 공개되면서 은폐 의혹이 커지고 있습니다. 마산 지역은 이미 3.15 시위 이후 긴장 상태가 지속되고 있었습니다."},
|
| 123 |
+
"action_419_t2_opt1_text": {"보통": "김주열 군 시신 안치 병원을 찾아 부검의 또는 의료진 접촉 시도"},
|
| 124 |
+
"action_419_t2_opt2_text": {"보통": "마산 시내 학생 시위 현장(마산상고, 북마산파출소 등) 직접 취재"},
|
| 125 |
+
"action_419_t2_opt3_text": {"보통": "최초 시신 발견 어민 및 김주열 군 가족 인터뷰"},
|
| 126 |
+
"info_419_t2_opt1_got": {"보통": "정보: 병원 관계자(익명), '부검 결과 김 군의 사인은 최루탄 파편에 의한 두개골 골절 및 뇌손상으로 잠정 결론났다. 경찰이 부검 결과 발표를 미루고 있다'고 전함. (키워드: 최루탄 사인, 두개골 골절, 경찰 발표 지연)"},
|
| 127 |
+
"info_419_t2_opt2_got": {"보통": "정보: 마산상고 학생들이 '학우를 살려내라!', '살인경찰 처단하라!' 등의 구호를 외치며 경찰과 격렬하게 대치 중. 돌과 화염병이 등장했으며, 경찰은 최루탄을 난사하며 강경 진압. 부상 학생 속출. (키워드: 마산 학생시위, 경찰과격진압, 부상자)"},
|
| 128 |
+
"info_419_t2_opt3_got": {"보통": "정보: 김주열 군 어머니, '내 아들은 시위 나간다고 말하고 나갔다. 경찰이 죽인 것이 틀림없다. 이 원통함을 어떻게 풀어야 할지 모르겠다'며 오열. (키워드: 김주열 가족, 경찰 책임 주장, 원통함)"},
|
| 129 |
+
# Turn 3 (고려대생 피습)
|
| 130 |
+
"event_419_t3_assignment": {"보통": "4월 18일, 부정선거 규탄 시위를 마치고 귀교하던 고려대학교 학생들이 정치깡패들에게 피습당하는 사건이 발생했습니다. 사건의 전모, 경찰의 대응, 학생 및 시민 사회의 반응을 취재하십시오."},
|
| 131 |
+
"event_419_t3_source": {"보통": "[배경 정보] 김주열 열사 사건 이후 학생 시위가 전국적으로 확산되는 가운데, 조직폭력배(정치깡패)가 동원되어 시위 학생들을 폭행하는 사건이 발생하여 국민적 분노가 더욱 커지고 있습니다. 경찰의 묵인 또는 방조 의혹도 제기되고 있습니다."},
|
| 132 |
+
"action_419_t3_opt1_text": {"보통": "피습 현장(종로4가) 및 피해 학생 입원 병원 방문 취재"},
|
| 133 |
+
"action_419_t3_opt2_text": {"보통": "경찰서 방문, 사건 수사 진행 상황 및 정치깡패 배후설 관련 질의"},
|
| 134 |
+
"action_419_t3_opt3_text": {"보통": "고려대학교 총학생회 및 다른 대학 학생 대표 인터뷰"},
|
| 135 |
+
"info_419_t3_opt1_got": {"보통": "정보: 피해 학생 증언, '시위 후 평화롭게 돌아가는데 갑자기 쇠파이프와 각목을 든 수십 명의 괴한이 덮쳤다. 살려달라고 외쳤지만 무자비하게 폭행했다.' 병원 복도에는 부상당한 학생들로 가득. (키워드: 고려대생 피습, 정치깡패, 쇠파이프, 집단폭행)"},
|
| 136 |
+
"info_419_t3_opt2_got": {"보통": "정보: 경찰 관계자, '우발적인 학생 간 충돌로 파악하고 있다. 정치깡패 배후설은 낭설이다'라며 수사 확대에 소극적인 태도. (키워드: 경찰 축소수사, 우발적 충돌 주장, 배후부인)"},
|
| 137 |
+
"info_419_t3_opt3_got": {"보통": "정보: 서울대 총학생회장, '이는 명백한 야만적 테러 행위이며, 정권의 비호 없이는 불가능한 일이다. 모든 학생이 연대하여 투쟁할 것'이라고 강력 규탄. (키워드: 학생사회 규탄, 정권 배후 의혹, 연대투쟁)"},
|
| 138 |
+
# Turn 4 (4.19 당일, 경무대 앞 발포)
|
| 139 |
+
"event_419_t4_assignment": {"보통": "4월 19일, 서울 시내에서 대규모 시위가 발생하여 경무대로 향하고 있습니다. 시위대의 규모와 요구사항, 경찰의 대응(발포 여부 포함), 사상자 발생 현황을 긴급 타전해야 합니다. 극도로 위험한 상황이니 신변 안전에 각별히 유의하십시오."},
|
| 140 |
+
"event_419_t4_source": {"보통": "[배경 정보] 고려대생 피습 사건에 분노한 서울 시내 대학생과 중고생, 시민들이 거리로 쏟아져 나와 '이승만 하야', '부정선거 다시 하라' 등을 외치며 경무대로 향하고 있습니다. 경찰은 최루탄과 소방차를 동원해 저지하고 있으나 역부족인 상황입니다."},
|
| 141 |
+
"action_419_t4_opt1_text": {"보통": "경무대 인근 시위대 선두 합류, 현장 상황 실시간 취재 (매우 위험)"},
|
| 142 |
+
"action_419_t4_opt2_text": {"보통": "안전한 건물 옥상 등에서 시위 전체 규모 및 경찰 대응 관찰"},
|
| 143 |
+
"action_419_t4_opt3_text": {"보통": "병원 응급실 방문, 부상자 및 사망자 현황 파악"},
|
| 144 |
+
"info_419_t4_opt1_got": {"보통": "정보: 시위대, 경찰 저지선 뚫고 경무대 진입 시도. 경찰, 시위대를 향해 무차별 발포 시작. 눈앞에서 학생들이 피를 흘리며 쓰러지는 아비규환의 현장. (키워드: 경무대 발포, 무차별 사격, 학생 희생, 아비규환)"},
|
| 145 |
+
"info_419_t4_opt2_got": {"보통": "정보: 수만 명의 시위대가 태평로와 광화문 일대를 가득 메움. 경찰, 최루탄과 함께 실탄 발포 확인. 시가지는 전쟁터 방불. (키워드: 대규모 시위, 경찰 실탄 발포, 시가전 양상)"},
|
| 146 |
+
"info_419_t4_opt3_got": {"보통": "정보: 세브란스 병원 응급실, 총상 환자로 가득. 사망자 다수 발생. 의료진은 쉴 새 없이 움직이지만 역부족. '학생들이 죽어가고 있다'는 절규. (키워드: 총상환자, 다수 사망, 병원 참상, 의료진 부족)"},
|
| 147 |
+
# Turn 5 (계엄령 선포)
|
| 148 |
+
"event_419_t5_assignment": {"보통": "정부가 서울 등 주요 도시에 비상계엄을 선포했습니다. 계엄군의 배치 상황, 시민들의 반응, 그리고 계엄 하에서의 언론 통제 실태를 취재하십시오."},
|
| 149 |
+
"event_419_t5_source": {"보통": "[배경 정보] 4.19 시위가 격화되자 정부는 4월 19일 오후 5시를 기해 서울 지역에 비상계엄을 선포하고 군 병력을 투입했습니다. 이후 부산, 대구, 광주 등 주요 도시로 계엄이 확대되었습니다. 계엄사령부는 집회 및 시위 금지, 언론 검열 등을 포고했습니다."},
|
| 150 |
+
"action_419_t5_opt1_text": {"보통": "시내 주요 지점 계엄군 배치 및 검문검색 현황 취재"},
|
| 151 |
+
"action_419_t5_opt2_text": {"보통": "계엄 하 시민 인터뷰 (두려움 속에서도 저항 의지 확인)"},
|
| 152 |
+
"action_419_t5_opt3_text": {"보통": "계엄사령부 방문, 언론 검열 지침 및 협조 요청 내용 파악"},
|
| 153 |
+
"info_419_t5_opt1_got": {"보통": "정보: 탱크와 장갑차를 앞세운 계엄군이 시내 주요 도로와 관공서를 장악. 시민들의 통행을 엄격히 통제하며, 일부 지역에서는 학생들을 강제 연행하는 모습 목격. (키워드: 계엄군 시내 장악, 탱크 장갑차, 통행금지, 강제연행)"},
|
| 154 |
+
"info_419_t5_opt2_got": {"보통": "정보: 시민 A씨, '군인들이 총칼을 들고 거리를 활보하니 무섭지만, 이대로 독재를 용납할 수는 없다. 기회가 되면 다시 거리로 나갈 것'이라고 다짐. (키워드: 계엄하 시민반응, 공포와 저항, 독재타도 의지)"},
|
| 155 |
+
"info_419_t5_opt3_got": {"보통": "정보: 계엄사 공보실, '국가 안정을 위해 보도 내용을 사전 검열하며, 유언비어 유포 시 엄단할 것'이라는 내용의 보도지침 전달. 사실상 정부 발표 외 보도 금지. (키워드: 언론검열, 보도지침, 계엄사령부, 정보통제)"},
|
| 156 |
+
# Turn 6 (대학교수단 시위)
|
| 157 |
+
"event_419_t6_assignment": {"보통": "4월 25일, 전국 대학교수단이 시국선언문을 발표하고 거리 시위에 나섰습니다. 교수단의 요구사항, 시위 규모 및 양상, 이에 대한 정부와 시민들의 반응을 취재하십시오."},
|
| 158 |
+
"event_419_t6_source": {"보통": "[배경 정보] 학생들의 피에 지식인 사회도 분노했습니다. 서울대 문리대 교수들을 시작으로 전국 27개 대학 258명의 교수들이 '학생들의 피에 보답하라'며 이승만 대통령의 하야와 정부 총사퇴를 요구하는 시국선언을 하고 거리로 나섰습니다. 이는 정권에 큰 도덕적 타격을 주었습니다."},
|
| 159 |
+
"action_419_t6_opt1_text": {"보통": "교수단 시위 현장(국회의사당 앞 등) 동행 취재"},
|
| 160 |
+
"action_419_t6_opt2_text": {"보통": "시국선언 주도 교수 인터뷰 (요구사항 및 결의)"},
|
| 161 |
+
"action_419_t6_opt3_text": {"보통": "시민들의 교수단 시위 지지 및 환호 반응 취재"},
|
| 162 |
+
"info_419_t6_opt1_got": {"보통": "정보: 수백 명의 교수들이 '이승만은 하야하라', '민주주의 수호하자' 등의 플래카드를 들고 질서정연하게 행진. 시민들은 박수와 환호로 지지. 경찰도 이전보다 소극적 대응. (키워드: 교수단 시위, 이승만 하야 요구, 시민 지지, 평화 행진)"},
|
| 163 |
+
"info_419_t6_opt2_got": {"보통": "정보: 서울대 K교수, '더 이상 지식인으로서 불의를 외면할 수 없었다. 이 대통령은 즉각 물러나고, 민주 질서를 회복해야 한다'고 단호히 발언. (키워드: 교수 시국선언, 불의 항거, 민주질서 회복)"},
|
| 164 |
+
"info_419_t6_opt3_got": {"보통": "정보: 시위 지켜보던 시민 L씨, '교수님들까지 나서주시니 이제 정말 이 정권 끝났다. 우리 학생들, 시민들 힘내자!'며 눈시울을 붉힘. (키워드: 시민 감격, 교수 지지, 정권 종말 예감)"},
|
| 165 |
+
# Turn 7 (미국의 압력과 이승만 하야 결심)
|
| 166 |
+
"event_419_t7_assignment": {"보통": "미국 정부가 현 사태에 대한 우려와 함께 이승만 대통령의 결단을 촉구하는 메시지를 전달한 것으로 알려졌습니다. 이승만 대통령의 심경 변화와 하야 가능성, 그리고 자유당 내부의 움직임을 긴급 취재하십시오."},
|
| 167 |
+
"event_419_t7_source": {"보통": "[배경 정보] 4.19 혁명이 격화되자 미국 정부는 월터 매카나기 주한미국대사를 통해 이승만 대통령에게 사실상의 하야를 권고했습니다. 국제적인 고립과 내부의 거센 저항에 직면한 이 대통령은 중대한 결심을 앞두고 있습니다."},
|
| 168 |
+
"action_419_t7_opt1_text": {"보통": "경무대 주변 취재, 이 대통령 측근 동향 파악"},
|
| 169 |
+
"action_419_t7_opt2_text": {"보통": "자유당 고위 관계자 접촉, 당내 분위기 및 수습책 논의 내용 취재"},
|
| 170 |
+
"action_419_t7_opt3_text": {"보통": "주한미국대사관 주변 취재, 미국의 입장 변화 확인"},
|
| 171 |
+
"info_419_t7_opt1_got": {"보통": "정보: 경무대 관계자(익명), '대통령께서 며칠 밤잠을 못 이루시고 깊은 고뇌에 빠져 계신다. 곧 중대 발표가 있을 것이라는 소문이 파다하다.' (키워드: 이승만 고뇌, 중대발표 임박, 경무대 분위기)"},
|
| 172 |
+
"info_419_t7_opt2_got": {"보통": "정보: 자유당 중진 의원 M씨, '이미 대세는 기울었다. 대통령께서 명예롭게 퇴진하시는 것만이 유일한 수습책이다. 당내에서도 하야를 건의해야 한다는 목소리가 커지고 있다.' (키워드: 자유당 하야 건의, 명예퇴진, 당내 분열)"},
|
| 173 |
+
"info_419_t7_opt3_got": {"보통": "정보: 미 대사관 관계자(비공식), '미국 정부는 한국의 민주주의 회복과 안정적인 정권 이양을 강력히 희망한다. 현 상황이 지속되는 것은 바람직하지 않다.' (키워드: 미국 정부 입장, 민주주의 회복 촉구, 정권이양 희망)"},
|
| 174 |
+
# Turn 8 (이승만 대통령 하야 발표)
|
| 175 |
+
"event_419_t8_assignment": {"보통": "4월 26일, 이승만 대통령이 하야를 발표했습니다. 하야 성명 내용, 시민들의 반응, 그리고 향후 정국 전망을 종합적으로 취재하여 특별 기사를 작성해야 합니다. (이번 턴은 취재 활동 없이 바로 기사 작성으로 이어집니다.)"},
|
| 176 |
+
"event_419_t8_source": {"보통": "[속보] 이승만 대통령, 국민의 요구에 따라 대통령직에서 물러나겠다고 발표. 라디오를 통해 하야 성명 발표. 시민들 거리로 쏟아져 나와 환호. 12년간의 자유당 정권 종식."},
|
| 177 |
+
"action_419_t8_opt1_text": {"보통": "하야 성명 전문 및 주요 내용 분석"}, # 정보 획득용 선택지 (실제로는 자동 획득)
|
| 178 |
+
"action_419_t8_opt2_text": {"보통": "시민 환호 현장 및 인터뷰"},
|
| 179 |
+
"action_419_t8_opt3_text": {"보통": "정치권 및 학계 향후 정국 전망 분석"},
|
| 180 |
+
"info_419_t8_opt1_got": {"보통": "정보: 이승만 대통령 하야 성명 전문 입수. '국민이 원한다면 대통령직을 사임하겠다', '3.15 선거는 부정이 있었으니 다시 하도록 지시', '내각책임제 개헌도 할 것' 등 내용 포함. (키워드: 이승만 하야 성명, 국민의 뜻, 부정선거 인정, 내각제 개헌)"},
|
| 181 |
+
"info_419_t8_opt2_got": {"보통": "정보: 서울 시내, 시민들 거리로 뛰쳐나와 '독재 타도, 민주 승리' 외치며 서로 부둥켜안고 눈물. 자동차 경적 울리며 축제 분위기. (키워드: 시민 환호, 민주승리, 축제분위기, 독재종식)"},
|
| 182 |
+
"info_419_t8_opt3_got": {"보통": "정보: 정치학자 P교수, '허정 과도정부 수립 후 조속한 시일 내에 총선거 실시 예상. 민주당 중심의 내각책임제 정부 출범 가능성 높다. 다만, 정치적 혼란과 사회적 요구 분출 우려.' (키워드: 허정과도정부, 총선거, 내각책임제, 정치혼란 우려)"},
|
| 183 |
|
| 184 |
# --- 5.18 광주 취재 시나리오 ---
|
| 185 |
"scenario_518_gwangju_name": {"보통": "5.18 광주 현장 취재 (1980)"},
|
| 186 |
+
# Turn 1 (5.17 비상계엄 전국 확대)
|
| 187 |
+
"event_518_t1_assignment": {"보통": "5월 17일 24시를 기해 비상계엄이 전국으로 확대되었습니다. 광주 지역의 분위기, 주요 인사(학생운동 지도부, 재야인사)들의 동향, 그리고 계엄군의 움직임을 파악하여 보고하십시오. 언론 통제가 심하니 보안에 각별히 유의해야 합니다."},
|
| 188 |
+
"event_518_t1_source": {"보통": "[배경 정보] 10.26 사건 이후 '서울의 봄'이 찾아왔으나, 신군부가 12.12 군사반란으로 실권을 장악하면서 정국은 다시 경색되고 있습니다. 신군부는 사회 혼란을 빌미로 비상계엄을 전국으로 확대하고, 김대중, 김영삼 등 주요 정치인들을 체포/연금했습니다. 광주 지역 대학가에서는 학생들의 시위 움직임이 감지되고 있습니다."},
|
| 189 |
+
"action_518_t1_opt1_text": {"보통": "전남대학교 등 주요 대학가 학생회관 주변 탐문"},
|
| 190 |
+
"action_518_t1_opt2_text": {"보통": "광주 시내 주요 도로 및 공공기관 계엄군 배치 상황 확인"},
|
| 191 |
+
"action_518_t1_opt3_text": {"보통": "지역 재야인사 또는 종교 지도자 비밀 인터뷰 시도"},
|
| 192 |
+
"info_518_t1_opt1_got": {"보통": "정보: 전남대 학생회 간부들, '계엄 확대는 민주화 열망을 짓밟는 폭거'라며 내일(18일) 오전 교내 시위 계획 중. '휴교령 반대, 계엄 철폐' 구호 준비. (키워드: 전남대 학생시위 예고, 계엄반대, 휴교령 철회 요구)"},
|
| 193 |
+
"info_518_t1_opt2_got": {"보통": "정보: 광주역, 버스터미널, 도청 등 주요 지점에 중무장한 공수부대원들이 배치되기 시작. 시민들은 불안감과 공포감을 느끼고 있음. (키워드: 공수부대 배치, 삼엄한 경계, 시민 불안)"},
|
| 194 |
+
"info_518_t1_opt3_got": {"보통": "정보: 익명의 종교 지도자, '신군부가 정권 장악을 위해 무자비한 탄압을 자행할 가능성이 크다. 광주가 희생양이 될까 우려된다'며 깊은 한숨. (키워드: 신군부 탄압 우려, 광주 희생양, 종교계 시국관)"},
|
| 195 |
+
# Turn 2 (5.18 첫날, 전남대 앞 충돌)
|
| 196 |
+
"event_518_t2_assignment": {"보통": "5월 18일 오전, 전남대학교 앞에서 학생들과 계엄군 간의 첫 충돌이 발생했습니다. 충돌의 원인과 과정, 학생 및 계엄군의 피해 상황, 그리고 시민들의 반응을 긴급히 취재하십시오. 현장 접근 시 신변 안전에 극히 유의해야 합니다."},
|
| 197 |
+
"event_518_t2_source": {"보통": "[배경 정보] 어제(17일) 비상계엄 전국 확대 조치에 따라 대학 휴교령이 내려졌으나, 전남대 학생들은 이에 항의하며 교문 앞에서 시위를 벌였습니다. 계엄군(공수부대)은 이를 강경 진압하기 시작했습니다."},
|
| 198 |
+
"action_518_t2_opt1_text": {"보통": "전남대학교 정문 앞 시위 현장 직접 취재 (매우 위험)"},
|
| 199 |
+
"action_518_t2_opt2_text": {"보통": "부상 학생 후송된 인근 병원 방문, 피해 상황 ���악"},
|
| 200 |
+
"action_518_t2_opt3_text": {"보통": "현장 주변 상인 및 주민 목격담 청취"},
|
| 201 |
+
"info_518_t2_opt1_got": {"보통": "정보: 공수부대원들이 교문으로 진입하려는 학생들을 향해 무차별적으로 곤봉을 휘두르고 군홧발로 폭행. 여학생, 남학생 가리지 않고 구타. 학생들이 피를 흘리며 쓰러지고 끌려가는 모습 목격. (키워드: 공수부대 과잉진압, 무차별 폭행, 학생 부상, 전남대 정문)"},
|
| 202 |
+
"info_518_t2_opt2_got": {"보통": "정보: 병원 응급실, 두부 외상 및 골절상 입은 학생들로 가득. 한 학생은 '단지 학교에 들어가려 했을 뿐인데, 군인들이 갑자기 몽둥이로 때렸다'며 울분 토로. (키워드: 학생 중상, 병원 응급실, 군인 폭력 증언)"},
|
| 203 |
+
"info_518_t2_opt3_got": {"보통": "정보: 인근 상인, '학생들이 무슨 큰 죄를 지었다고 저렇게까지 때리나. 이건 너무 심하다. 군인들이 아니라 깡패 같다'며 분개. (키워드: 시민 분노, 계엄군 만행 비판, 과잉진압 목격)"},
|
| 204 |
+
# (이하 5.18 턴들 생략)
|
| 205 |
|
| 206 |
# --- 6월 항쟁 취재 시나리오 ---
|
| 207 |
"scenario_june_struggle_name": {"보통": "6월 항쟁 동행 취재 (1987)"},
|
| 208 |
+
# Turn 1 (박종철 고문치사 사건)
|
| 209 |
+
"event_june_t1_assignment": {"보통": "서울대생 박종철 군이 경찰 조사 중 사망하는 사건이 발생했습니다. 경찰은 '책상을 탁 치니 억 하고 죽었다'고 발표했으나, 고문 의혹이 강하게 제기되고 있습니다. 사건의 진실을 규명할 단서와 주변 반응을 취재하십시오."},
|
| 210 |
+
"event_june_t1_source": {"보통": "[배경 정보] 1987년 1월 14일, 박종철 군은 치안본부 남영동 대공분실에서 조사를 받던 중 사망했습니다. 경찰의 석연치 않은 사인 발표는 국민적 공분을 사고 있으며, 재야단체와 종교계를 중심으로 진상 규명 요구가 확산되고 있습니다. 전두환 정권의 도덕성에 큰 타격이 예상됩니다."},
|
| 211 |
+
"action_june_t1_opt1_text": {"보통": "사건 담당 경찰서 및 치안본부 주변 취재, 관계자 비공식 접촉 시도"},
|
| 212 |
+
"action_june_t1_opt2_text": {"보통": "박종철 군 유족 및 서울대 동료 학생 인터뷰"},
|
| 213 |
+
"action_june_t1_opt3_text": {"보통": "부검 참관 의사 또는 의료계 전문가 익명 인터뷰 (사인 규명)"},
|
| 214 |
+
"info_june_t1_opt1_got": {"보통": "정보: 치안본부 관계자(익명), '윗선에서 사건을 빨리 덮으라는 지시가 있었다. 단순 쇼크사로 처리하라는 압력이 상당하다'고 귀띔. (키워드: 경찰 윗선 압력, 사건은폐 지시, 쇼크사 조작)"},
|
| 215 |
+
"info_june_t1_opt2_got": {"보통": "정보: 박종철 군 아버지는 '철아, 잘 가그래이... 아부지는 할 말이 없데이...'라며 오열. 동료 학생들은 '고문살인 책임자를 처벌하라'며 분노. (키워드: 박종철유족 슬픔, 고문살인 규탄, 학생 분노)"},
|
| 216 |
+
"info_june_t1_opt3_got": {"보통": "정보: 부검의 황적준 박사(실명 언급 가능성 낮으나 게임적 허용), '부검 결과 목 부위 압박 흔적과 폐에서 수포음이 들리는 등 단순 쇼크사와는 거리가 멀다. 물고문 가능성을 배제할 수 없다'고 조심스럽게 언급. (키워드: 부검의 증언, 물고문 의혹, 경찰발표 반박)"},
|
| 217 |
+
# (이하 6월 항쟁 턴들 생략)
|
| 218 |
|
| 219 |
"article_tone_fact_based": {"보통": "객관적 사실 전달 위주"},
|
| 220 |
"article_tone_critical": {"보통": "정부/기관 비판적 논조"},
|
|
|
|
| 224 |
"glossary_419_revolution_free_party": {"보통": "**자유당 (自由黨):** 1951년 이승만 대통령을 중심으로 창당된 정당. 1960년 4.19 혁명으로 이승만 대통령이 하야하면서 사실상 해체되었다."},
|
| 225 |
"glossary_419_revolution_315_election": {"보통": "**3.15 부정선거:** 1960년 3월 15일 실시된 제4대 대통령 및 제5대 부통령 선거에서 자유당 정권이 이승만-이기붕 후보를 당선시키기 위해 자행한 대규모 부정선거. 4.19 혁명의 직접적인 도화선이 되었다."},
|
| 226 |
"glossary_419_revolution_kimjuyeol": {"보통": "**김주열 (金朱烈):** 1943~1960. 마산상업고등학교 학생. 3.15 마산 시위 중 실종되었다가 4월 11일 눈에 최루탄이 박힌 시신으로 발견되어 4.19 혁명을 격화시키는 계기가 되었다."},
|
| 227 |
+
"glossary_419_revolution_gyeongmudae": {"보통": "**경무대 (景武臺):** 현재 청와대 자리에 있던 조선시대 건물로, 대한민국 정부 수립 후 대통령 관저 및 집무실로 사용되었다. 4.19 혁명 당시 시위대가 경무대로 향하자 경찰이 발포하여 많은 사상자가 발생했다."},
|
| 228 |
+
"glossary_419_revolution_martial_law": {"보통": "**계엄령 (戒嚴令):** 전시, 사변 또는 이에 준하는 국가비상사태 시 법률이 정하는 바에 따라 군사권을 발동하여 치안을 유지할 수 있는 국가긴급권의 하나. 4.19 혁명 당시 정부는 주요 도시에 계엄령을 선포했다."},
|
| 229 |
+
|
| 230 |
"glossary_518_gwangju_new_military": {"보통": "**신군부 (新軍部):** 1979년 10.26 사건 이후 12.12 군사반란을 통해 권력을 장악한 전두환, 노태우 등 하나회 중심의 군부 세력."},
|
| 231 |
+
"glossary_518_gwangju_517_measure": {"보통": "**5.17 비상계엄 전국 확대 조치:** 1980년 5월 17일 신군부가 비상계엄을 전국으로 확대하고 정치활동 금지, 대학 휴교, 언론 검열 강화 등을 단행한 조치. 5.18 광주 민주화운동의 직접적인 배경이 되었다."},
|
| 232 |
+
"glossary_518_gwangju_paratroopers": {"보통": "**공수부대 (공수특전여단):** 5.18 광주 민주화운동 당시 광주에 투입되어 학생과 시민들을 과잉 진압한 부대. 잔혹한 진압 방식으로 많은 사상자를 발생시켰다."},
|
| 233 |
+
"glossary_518_gwangju_geumnamno": {"보통": "**금남로 (錦南路):** 광주광역시의 중심 도로. 5.18 광주 민주화운동 기간 동안 대규모 시위와 계엄군과의 충돌이 벌어진 핵심 장소였다."},
|
| 234 |
+
"glossary_518_gwangju_citizens_army": {"보통": "**시민군 (市民軍):** 5.18 광주 민주화운동 당시 계엄군의 폭력적인 진압에 맞서 스스로 무장한 광주 시민들. 도청을 중심으로 항쟁을 벌였다."},
|
| 235 |
+
"glossary_518_gwangju_sangmuchungjeong": {"보통": "**상무충정작전 (尙武忠正作戰):** 1980년 5월 27일 새벽 계엄군이 광주 시내로 재진입하여 전남도청 등을 장악한 군사작전명. 이 작전으로 5.18 광주 민주화운동은 비극적으로 진압되었다."},
|
| 236 |
+
|
| 237 |
"glossary_june_struggle_parkjongchul": {"보통": "**박종철 (朴鍾哲):** 1965~1987. 서울대학교 언어학과 학생. 1987년 1월 경찰의 고문으로 사망하여 6월 민주 항쟁의 도화선이 되었다."},
|
| 238 |
+
"glossary_june_struggle_413_measure": {"보통": "**4.13 호헌 조치 (四一三護憲措置):** 1987년 4월 13일 전두환 대통령이 대통령 직선제 개헌 요구를 거부하고 현행 헌법(간선제)을 고수하겠다고 발표한 조치. 국민적 저항을 불러일으켰다."},
|
| 239 |
+
"glossary_june_struggle_leehanyeol": {"보통": "**이한열 (李韓烈):** 1966~1987. 연세대학교 경영학과 학생. 1987년 6월 9일 시위 중 경찰이 쏜 최루탄에 맞아 중태에 빠졌다가 7월 5일 사망했다. 그의 죽음은 6월 항쟁을 더욱 확산시키는 계기가 되었다."},
|
| 240 |
+
"glossary_june_struggle_610_rally": {"보통": "**6.10 국민대회:** 1987년 6월 10일 '박종철군 고문살인 은폐 규탄 및 민주헌법 쟁취 범국민대회'라는 이름으로 전국적으로 열린 대규모 시위. 6월 항쟁의 본격적인 시작을 알렸다."},
|
| 241 |
+
"glossary_june_struggle_myeongdong_cathedral": {"보통": "**명동성당 (明洞聖堂):** 서울 명동에 위치한 천주교 서울대교구 주교좌 성당. 6월 항쟁 당시 시위대의 주요 농성 장소이자 민주화 운동의 상징적 공간이 되었다."},
|
| 242 |
+
"glossary_june_struggle_necktie_brigade": {"보통": "**넥타이 부대:** 6월 항쟁 당시 시위에 참여한 일반 사무직 회사원들을 일컫는 말. 중산층의 시위 참여를 상징하며 항쟁의 대중적 확산을 보여주었다."},
|
| 243 |
+
"glossary_june_struggle_629_declaration": {"보통": "**6.29 선언:** 1987년 6월 29일 당시 민정당 대표위원 노태우가 발표한 시국 수습 특별 선언. 대통령 직선제 개헌, 김대중 사면복권 등 8개항의 민주화 조치를 내용으로 하며, 6월 항쟁의 실질적인 승리를 의미했다."},
|
| 244 |
}
|
| 245 |
|
| 246 |
# --- 어휘 조정 함수 ---
|
|
|
|
| 267 |
'game_year': scenario_settings['start_year'],
|
| 268 |
'status': {
|
| 269 |
'press_freedom': scenario_settings['initial_press_freedom'],
|
| 270 |
+
'article_score_total': 0,
|
| 271 |
+
'article_count': 0,
|
| 272 |
'reporter_safety': scenario_settings['initial_reporter_safety'],
|
| 273 |
'public_trust': scenario_settings['initial_public_trust'],
|
| 274 |
},
|
|
|
|
| 282 |
"4.19_revolution": [
|
| 283 |
{"turn": 1, "assignment_key": "event_419_t1_assignment", "source_key": "event_419_t1_source",
|
| 284 |
"options": [
|
| 285 |
+
{"action_key": "action_419_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 20, "info_key": "info_419_t1_opt1_got"},
|
| 286 |
+
{"action_key": "action_419_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 5, "info_key": "info_419_t1_opt2_got"},
|
| 287 |
+
{"action_key": "action_419_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_419_t1_opt3_got"},
|
| 288 |
+
], "article_writing_phase": True},
|
|
|
|
| 289 |
{"turn": 2, "assignment_key": "event_419_t2_assignment", "source_key": "event_419_t2_source",
|
| 290 |
"options": [
|
| 291 |
+
{"action_key": "action_419_t2_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "info_key": "info_419_t2_opt1_got"},
|
| 292 |
+
{"action_key": "action_419_t2_opt2_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_419_t2_opt2_got"},
|
| 293 |
+
{"action_key": "action_419_t2_opt3_text", "cost_freedom_risk": 20, "safety_risk": 10, "info_key": "info_419_t2_opt3_got"},
|
| 294 |
+
], "article_writing_phase": True},
|
| 295 |
+
{"turn": 3, "assignment_key": "event_419_t3_assignment", "source_key": "event_419_t3_source",
|
| 296 |
+
"options": [
|
| 297 |
+
{"action_key": "action_419_t3_opt1_text", "cost_freedom_risk": 10, "safety_risk": 20, "info_key": "info_419_t3_opt1_got"},
|
| 298 |
+
{"action_key": "action_419_t3_opt2_text", "cost_freedom_risk": 15, "safety_risk": 5, "info_key": "info_419_t3_opt2_got"},
|
| 299 |
+
{"action_key": "action_419_t3_opt3_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_419_t3_opt3_got"},
|
| 300 |
+
], "article_writing_phase": True},
|
| 301 |
+
{"turn": 4, "assignment_key": "event_419_t4_assignment", "source_key": "event_419_t4_source",
|
| 302 |
+
"options": [
|
| 303 |
+
{"action_key": "action_419_t4_opt1_text", "cost_freedom_risk": 25, "safety_risk": 35, "info_key": "info_419_t4_opt1_got"},
|
| 304 |
+
{"action_key": "action_419_t4_opt2_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_419_t4_opt2_got"},
|
| 305 |
+
{"action_key": "action_419_t4_opt3_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_419_t4_opt3_got"},
|
| 306 |
+
], "article_writing_phase": True},
|
| 307 |
+
{"turn": 5, "assignment_key": "event_419_t5_assignment", "source_key": "event_419_t5_source",
|
| 308 |
+
"options": [
|
| 309 |
+
{"action_key": "action_419_t5_opt1_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_419_t5_opt1_got"},
|
| 310 |
+
{"action_key": "action_419_t5_opt2_text", "cost_freedom_risk": 15, "safety_risk": 20, "info_key": "info_419_t5_opt2_got"},
|
| 311 |
+
{"action_key": "action_419_t5_opt3_text", "cost_freedom_risk": 20, "safety_risk": 5, "info_key": "info_419_t5_opt3_got"},
|
| 312 |
+
], "article_writing_phase": True},
|
| 313 |
+
{"turn": 6, "assignment_key": "event_419_t6_assignment", "source_key": "event_419_t6_source",
|
| 314 |
+
"options": [
|
| 315 |
+
{"action_key": "action_419_t6_opt1_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_419_t6_opt1_got"},
|
| 316 |
+
{"action_key": "action_419_t6_opt2_text", "cost_freedom_risk": 10, "safety_risk": 5, "info_key": "info_419_t6_opt2_got"},
|
| 317 |
+
{"action_key": "action_419_t6_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "info_key": "info_419_t6_opt3_got"},
|
| 318 |
+
], "article_writing_phase": True},
|
| 319 |
+
{"turn": 7, "assignment_key": "event_419_t7_assignment", "source_key": "event_419_t7_source",
|
| 320 |
+
"options": [
|
| 321 |
+
{"action_key": "action_419_t7_opt1_text", "cost_freedom_risk": 10, "safety_risk": 10, "info_key": "info_419_t7_opt1_got"},
|
| 322 |
+
{"action_key": "action_419_t7_opt2_text", "cost_freedom_risk": 15, "safety_risk": 5, "info_key": "info_419_t7_opt2_got"},
|
| 323 |
+
{"action_key": "action_419_t7_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "info_key": "info_419_t7_opt3_got"},
|
| 324 |
+
], "article_writing_phase": True},
|
| 325 |
+
{"turn": 8, "assignment_key": "event_419_t8_assignment", "source_key": "event_419_t8_source",
|
| 326 |
+
"options": [ # 이 턴은 정보 획득 선택지가 의미 없을 수 있으나, 형식상 유지
|
| 327 |
+
{"action_key": "action_419_t8_opt1_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_419_t8_opt1_got"},
|
| 328 |
+
{"action_key": "action_419_t8_opt2_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_419_t8_opt2_got"},
|
| 329 |
+
{"action_key": "action_419_t8_opt3_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_419_t8_opt3_got"},
|
| 330 |
+
], "article_writing_phase": True, "is_final_turn_event": True}, # 마지막 턴 표시
|
| 331 |
],
|
| 332 |
"5.18_gwangju": [
|
| 333 |
{"turn": 1, "assignment_key": "event_518_t1_assignment", "source_key": "event_518_t1_source",
|
| 334 |
"options": [
|
| 335 |
+
{"action_key": "action_518_t1_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "info_key": "info_518_t1_opt1_got"},
|
| 336 |
+
{"action_key": "action_518_t1_opt2_text", "cost_freedom_risk": 10, "safety_risk": 20, "info_key": "info_518_t1_opt2_got"},
|
| 337 |
+
{"action_key": "action_518_t1_opt3_text", "cost_freedom_risk": 20, "safety_risk": 30, "info_key": "info_518_t1_opt3_got"},
|
| 338 |
+
], "article_writing_phase": False}, # 초기에는 정보 수집만
|
| 339 |
+
{"turn": 2, "assignment_key": "event_518_t2_assignment", "source_key": "event_518_t2_source",
|
| 340 |
+
"options": [
|
| 341 |
+
{"action_key": "action_518_t2_opt1_text", "cost_freedom_risk": 30, "safety_risk": 40, "info_key": "info_518_t2_opt1_got"},
|
| 342 |
+
{"action_key": "action_518_t2_opt2_text", "cost_freedom_risk": 15, "safety_risk": 20, "info_key": "info_518_t2_opt2_got"},
|
| 343 |
+
{"action_key": "action_518_t2_opt3_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_518_t2_opt3_got"},
|
| 344 |
+
], "article_writing_phase": True},
|
| 345 |
+
# (이하 5.18 턴들 추가 필요 - 최대 8턴까지)
|
| 346 |
+
# 예시: 마지막 턴
|
| 347 |
+
{"turn": 8, "assignment_key": "event_518_t8_assignment", "source_key": "event_518_t8_source", # 도청 진압 이후 상황
|
| 348 |
+
"options": [
|
| 349 |
+
{"action_key": "action_518_t8_opt1_text", "cost_freedom_risk": 25, "safety_risk": 30, "info_key": "info_518_t8_opt1_got"},
|
| 350 |
+
{"action_key": "action_518_t8_opt2_text", "cost_freedom_risk": 30, "safety_risk": 20, "info_key": "info_518_t8_opt2_got"},
|
| 351 |
+
], "article_writing_phase": True, "is_final_turn_event": True},
|
| 352 |
],
|
| 353 |
"june_struggle": [
|
| 354 |
{"turn": 1, "assignment_key": "event_june_t1_assignment", "source_key": "event_june_t1_source",
|
| 355 |
"options": [
|
| 356 |
+
{"action_key": "action_june_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 15, "info_key": "info_june_t1_opt1_got"},
|
| 357 |
+
{"action_key": "action_june_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 10, "info_key": "info_june_t1_opt2_got"},
|
| 358 |
+
{"action_key": "action_june_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "info_key": "info_june_t1_opt3_got"},
|
| 359 |
+
], "article_writing_phase": True},
|
| 360 |
+
# (이하 6월 항쟁 턴들 추가 필요 - 최대 7턴까지)
|
| 361 |
+
# 예시: 마지막 턴
|
| 362 |
+
{"turn": 7, "assignment_key": "event_june_t7_assignment", "source_key": "event_june_t7_source", # 6.29 선언
|
| 363 |
+
"options": [
|
| 364 |
+
{"action_key": "action_june_t7_opt1_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_june_t7_opt1_got"},
|
| 365 |
+
{"action_key": "action_june_t7_opt2_text", "cost_freedom_risk": 0, "safety_risk": 0, "info_key": "info_june_t7_opt2_got"},
|
| 366 |
+
], "article_writing_phase": True, "is_final_turn_event": True},
|
| 367 |
]
|
| 368 |
}
|
|
|
|
| 369 |
# --- 다음 취재 지시 가져오기 ---
|
| 370 |
def get_next_assignment(scenario_key, current_turn, game_state):
|
| 371 |
scenario_assignments = HISTORICAL_ASSIGNMENTS.get(scenario_key, [])
|
|
|
|
| 379 |
"source_text": get_text_for_assignment(assignment_data.get("source_key", "")),
|
| 380 |
"options": [],
|
| 381 |
"article_writing_phase": assignment_data.get("article_writing_phase", False),
|
| 382 |
+
"is_final_turn_event": assignment_data.get("is_final_turn_event", False) # 마지막 턴인지 여부
|
| 383 |
}
|
| 384 |
for opt_data in assignment_data.get("options", []):
|
| 385 |
final_assignment["options"].append({
|
| 386 |
"action_text": get_text_for_assignment(opt_data["action_key"]),
|
| 387 |
"cost_freedom_risk": opt_data.get("cost_freedom_risk", 0),
|
| 388 |
"safety_risk": opt_data.get("safety_risk", 0),
|
|
|
|
| 389 |
"info_key": opt_data.get("info_key", "")
|
| 390 |
})
|
| 391 |
|
|
|
|
| 411 |
status['reporter_safety'] -= safety_loss
|
| 412 |
game_state['event_log'].append(get_text_for_processing("log_safety_loss").format(loss=safety_loss))
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
info_text = ""
|
| 415 |
if selected_option.get("info_key"):
|
| 416 |
info_text = get_text_for_processing(selected_option["info_key"])
|
|
|
|
| 437 |
for idx, note in enumerate(game_state['reporter_notebook']):
|
| 438 |
st.markdown(f"- {note}")
|
| 439 |
else:
|
| 440 |
+
st.caption("아직 취재한 내용이 없습니다. 취재 활동을 통해 정보를 수집하세요.")
|
| 441 |
st.markdown("---")
|
| 442 |
|
| 443 |
with st.form(key="article_form"):
|
| 444 |
+
article_headline = st.text_input(get_text_func("article_headline_label"), placeholder="예: 3.15 부정선거, 그 추악한 실태 고발")
|
| 445 |
+
article_body_summary = st.text_area(get_text_func("article_body_label"), height=150, placeholder="예: 오늘 치러진 정부통령 선거에서 자유당은 조직적인 부정행위를 자행했다. 투표함 바꿔치기, 공개투표 강요 등 민주주의를 유린하는 행태가 곳곳에서 목격되었다...")
|
| 446 |
|
| 447 |
article_tones = {
|
| 448 |
"fact_based": get_text_func("article_tone_fact_based"),
|
|
|
|
| 463 |
st.warning(get_text_func("warning_empty_article"))
|
| 464 |
return None
|
| 465 |
|
| 466 |
+
if not game_state['reporter_notebook']: # 노트 내용 없으면 제출 불가
|
| 467 |
+
st.warning("취재 노트에 내용이 없습니다. 최소한의 취재 정보가 있어야 기사를 작성할 수 있습니다.")
|
| 468 |
+
return None
|
| 469 |
+
|
| 470 |
return {
|
| 471 |
"turn": game_state['current_turn'],
|
| 472 |
"headline": article_headline,
|
|
|
|
| 476 |
}
|
| 477 |
return None
|
| 478 |
|
| 479 |
+
# --- 기사 평가 및 데스크 피드백 (OpenAI 사용) ---
|
| 480 |
+
def evaluate_article_and_get_feedback_openai(article, game_state, assignment_data):
|
| 481 |
status = game_state['status']
|
| 482 |
vocab_level = SCENARIOS[game_state['scenario_key']]["vocab_level"]
|
| 483 |
get_text_func = lambda k: get_text(k, vocab_level)
|
| 484 |
|
| 485 |
+
current_assignment_text = get_text_func(assignment_data["assignment_key"])
|
| 486 |
+
reporter_notes_str = "\n".join([f"- {note}" for note in article['raw_notes']])
|
| 487 |
+
article_tone_text = get_text_func(f"article_tone_{article['tone']}")
|
| 488 |
+
|
| 489 |
+
prompt = f"""
|
| 490 |
+
당신은 대한민국 현대사를 다루는 신문사의 베테랑 편집장입니다. 신입 기자가 아래와 같은 취재 지시를 받고, 수집한 노트 내용을 바탕으로 기사를 작성했습니다.
|
| 491 |
+
|
| 492 |
+
[취재 지시 내용]
|
| 493 |
+
{current_assignment_text}
|
| 494 |
+
|
| 495 |
+
[기자가 수집한 노트 내용]
|
| 496 |
+
{reporter_notes_str}
|
| 497 |
+
|
| 498 |
+
[기자가 작성한 기사]
|
| 499 |
+
- 제목: {article['headline']}
|
| 500 |
+
- 본문 요약: {article['body_summary']}
|
| 501 |
+
- 선택한 논조: {article_tone_text}
|
| 502 |
+
|
| 503 |
+
[평가 지침]
|
| 504 |
+
1. **사실 부합도 및 중요 정보 포함 여부:** 기사가 취재 노트의 핵심 정보와 취재 지시의 주요 사항을 얼마나 잘 반영하고 있는가? 중요한 사실이 누락되거나 왜곡되지는 않았는가? (40점 만점)
|
| 505 |
+
2. **논리성 및 명확성:** 기사의 내용이 논리적으로 전개되며, 독자가 이해하기 쉽게 명확하게 작성되었는가? (30점 만점)
|
| 506 |
+
3. **논조의 적절성 및 영향력:** 선택한 논조가 현재 상황과 기사 내용에 적절하며, 사회에 긍정적 또는 부정적 영향을 미칠 가능성은 어떠한가? (기자의 안전도 고려) (30점 만점)
|
| 507 |
+
|
| 508 |
+
위 평가 지침에 따라 각 항목별로 간략한 코멘트와 함께 점수를 부여하고, 총점(100점 만점)과 종합적인 피드백(2-3문장)을 제공해주십시오.
|
| 509 |
+
피드백은 기자가 발전할 수 있도록 구체적이고 건설적인 내용으로 작성해주세요.
|
| 510 |
+
|
| 511 |
+
출력 형식은 다음과 같이 JSON 형태로 제공해주세요:
|
| 512 |
+
{{
|
| 513 |
+
"score_fact": 점수(숫자),
|
| 514 |
+
"comment_fact": "코멘트(문자열)",
|
| 515 |
+
"score_logic": 점수(숫자),
|
| 516 |
+
"comment_logic": "코멘트(문자열)",
|
| 517 |
+
"score_tone": 점수(숫자),
|
| 518 |
+
"comment_tone": "코멘트(문자열)",
|
| 519 |
+
"total_score": 총점(숫자),
|
| 520 |
+
"overall_feedback": "종합 피드백(문자열)"
|
| 521 |
+
}}
|
| 522 |
+
"""
|
| 523 |
+
try:
|
| 524 |
+
with st.spinner("AI 편집장이 기사를 평가 중입니다..."):
|
| 525 |
+
response = client.chat.completions.create(
|
| 526 |
+
model="gpt-4o-mini", # 또는 gpt-4o
|
| 527 |
+
messages=[{"role": "user", "content": prompt}],
|
| 528 |
+
response_format={"type": "json_object"},
|
| 529 |
+
temperature=0.5,
|
| 530 |
+
max_tokens=600 # 토큰 수 넉넉하게
|
| 531 |
+
)
|
| 532 |
+
ai_evaluation_str = response.choices[0].message.content
|
| 533 |
+
ai_evaluation = json.loads(ai_evaluation_str)
|
| 534 |
+
|
| 535 |
+
total_score = ai_evaluation.get("total_score", 0)
|
| 536 |
+
overall_feedback = ai_evaluation.get("overall_feedback", "AI 평가를 가���오지 못했습니다.")
|
| 537 |
+
|
| 538 |
+
except Exception as e:
|
| 539 |
+
st.error(get_text_func("error_openai_api").format(error=str(e)))
|
| 540 |
+
total_score = random.randint(40, 70)
|
| 541 |
+
overall_feedback = "AI 편집장 시스템 오류로 자동 평가되었습니다. 내용은 훌륭하지만, 조금 더 팩트 체크에 신경 써주세요."
|
| 542 |
|
| 543 |
+
trust_change = (total_score - 50) // 5
|
| 544 |
status['public_trust'] = max(0, min(100, status['public_trust'] + trust_change))
|
|
|
|
|
|
|
| 545 |
|
| 546 |
+
if article['tone'] == "critical" and total_score > 60:
|
| 547 |
+
freedom_change = -random.randint(5, 10)
|
| 548 |
+
safety_change = -random.randint(3, 8)
|
| 549 |
+
status['press_freedom'] = max(0, min(100, status['press_freedom'] + freedom_change))
|
| 550 |
+
status['reporter_safety'] = max(0, min(100, status['reporter_safety'] + safety_change))
|
| 551 |
+
if freedom_change != 0: game_state['event_log'].append(get_text_func("log_freedom_change_article").format(change=freedom_change))
|
| 552 |
+
if safety_change != 0: game_state['event_log'].append(get_text_func("log_safety_change_article").format(change=safety_change))
|
| 553 |
+
|
| 554 |
+
status['article_score_total'] += total_score
|
| 555 |
+
status['article_count'] += 1
|
| 556 |
|
| 557 |
+
tone_display_text = get_text_func(f"article_tone_{article['tone']}")
|
| 558 |
+
game_state['event_log'].append(get_text_func("log_article_submitted").format(headline=article['headline'], tone=tone_display_text, score=total_score))
|
| 559 |
+
game_state['event_log'].append(get_text_func("log_desk_feedback").format(feedback=overall_feedback))
|
| 560 |
if trust_change != 0: game_state['event_log'].append(get_text_func("log_trust_change").format(change=trust_change))
|
|
|
|
|
|
|
| 561 |
|
| 562 |
game_state['submitted_articles'].append(article)
|
| 563 |
game_state['reporter_notebook'] = []
|
| 564 |
|
| 565 |
+
return overall_feedback
|
| 566 |
|
| 567 |
# --- UI 표시 함수들 ---
|
| 568 |
def display_reporter_dashboard(game_state):
|
|
|
|
| 571 |
get_text_for_ui = lambda k: get_text(k, vocab_level)
|
| 572 |
status = game_state['status']
|
| 573 |
|
| 574 |
+
avg_score = (status['article_score_total'] / status['article_count']) if status['article_count'] > 0 else 0
|
| 575 |
+
|
| 576 |
col1, col2, col3, col4 = st.columns(4)
|
| 577 |
with col1: st.metric(get_text_for_ui("term_public_trust"), f"{status['public_trust']}%")
|
| 578 |
with col2: st.metric(get_text_for_ui("term_press_freedom"), f"{status['press_freedom']}%")
|
| 579 |
with col3: st.metric(get_text_for_ui("term_reporter_safety"), f"{status['reporter_safety']}%")
|
| 580 |
+
with col4: st.metric(get_text_for_ui("term_article_score_avg"), f"{avg_score:.1f}점")
|
| 581 |
+
|
| 582 |
|
| 583 |
def display_reporter_notebook(game_state, vocab_level):
|
| 584 |
get_text_func = lambda k: get_text(k, vocab_level)
|
|
|
|
| 662 |
st.subheader("송고한 주요 기사 목록")
|
| 663 |
if game_state['submitted_articles']:
|
| 664 |
for article in game_state['submitted_articles']:
|
| 665 |
+
tone_text_key = f"article_tone_{article['tone']}"
|
| 666 |
+
tone_display_text = get_text_main(tone_text_key)
|
| 667 |
+
st.markdown(f"- **{article['headline']}** (논조: {tone_display_text}) - {article['turn']}일차 송고 (AI 점수: {game_state['submitted_articles'][-1].get('ai_score', 'N/A')}점)") # 마지막 기사 점수 표시 (개선 필요)
|
| 668 |
else:
|
| 669 |
st.caption("이번 취재 기간 동안 송고한 기사가 없습니다.")
|
| 670 |
|
|
|
|
| 713 |
st.session_state.desk_feedback_message = None
|
| 714 |
st.session_state.game_mode = 'reporter_action'
|
| 715 |
st.rerun()
|
| 716 |
+
else: # 모든 턴 종료
|
| 717 |
st.session_state.game_mode = 'assignment_over'
|
| 718 |
if game_state.get('event_log') is not None:
|
| 719 |
game_state['event_log'].append(get_text_main("log_assignment_over").format(scenario_name=SCENARIOS[scenario_key]['display_name']))
|
|
|
|
| 751 |
assignment_data = st.session_state.current_assignment_data
|
| 752 |
submitted_article = generate_article_interface(game_state, vocab_level)
|
| 753 |
if submitted_article:
|
| 754 |
+
feedback = evaluate_article_and_get_feedback_openai(submitted_article, game_state, assignment_data)
|
| 755 |
st.session_state.desk_feedback_message = feedback
|
| 756 |
+
st.session_state.game_mode = 'reporter_action' # 피드백 표시 후 다음날로 넘어갈 수 있도록
|
| 757 |
st.rerun()
|
| 758 |
|
| 759 |
if st.session_state.desk_feedback_message and st.session_state.game_mode == 'reporter_action':
|
| 760 |
st.divider()
|
| 761 |
st.subheader(get_text_main("desk_feedback_title"))
|
| 762 |
+
st.warning(st.session_state.desk_feedback_message) # AI 피드백 표시
|
| 763 |
|
| 764 |
+
with col_actions_notebook: # 오른쪽 컬럼
|
| 765 |
st.header("활동 기록 및 다음 단계")
|
| 766 |
can_proceed_to_next_day = False
|
| 767 |
if st.session_state.game_mode == 'reporter_action' and st.session_state.current_assignment_data:
|
| 768 |
assignment_data = st.session_state.current_assignment_data
|
| 769 |
if st.session_state.actions_taken_this_turn >= ACTIONS_PER_TURN_LIMIT:
|
| 770 |
+
if not assignment_data.get("article_writing_phase"): # 기사 작성 없는 턴
|
| 771 |
can_proceed_to_next_day = True
|
| 772 |
+
elif st.session_state.desk_feedback_message: # 기사 작성 후 피드백까지 받은 상태
|
| 773 |
can_proceed_to_next_day = True
|
| 774 |
|
| 775 |
if can_proceed_to_next_day:
|
|
|
|
| 777 |
current_turn = game_state['current_turn']
|
| 778 |
max_turns_for_scenario = SCENARIOS[scenario_key]['max_turns']
|
| 779 |
|
| 780 |
+
if current_turn >= max_turns_for_scenario or st.session_state.current_assignment_data.get("is_final_turn_event", False):
|
| 781 |
st.session_state.game_mode = 'assignment_over'
|
| 782 |
if game_state.get('event_log') is not None:
|
| 783 |
game_state['event_log'].append(get_text_main("log_assignment_over").format(scenario_name=SCENARIOS[scenario_key]['display_name']))
|
|
|
|
| 789 |
|
| 790 |
st.session_state.current_assignment_data = None
|
| 791 |
st.session_state.actions_taken_this_turn = 0
|
| 792 |
+
# desk_feedback_message는 다음 턴 시작 시 assignment_briefing에서 초기화
|
| 793 |
st.rerun()
|
| 794 |
|
| 795 |
st.divider()
|
|
|
|
| 821 |
keys_to_clear = ['current_scenario_key', 'game_state', 'current_assignment_data', 'actions_taken_this_turn', 'desk_feedback_message']
|
| 822 |
for k_to_clear in keys_to_clear:
|
| 823 |
if k_to_clear in st.session_state: del st.session_state[k_to_clear]
|
| 824 |
+
time.sleep(1) # 메시지 확인 시간
|
| 825 |
st.rerun()
|
| 826 |
|
| 827 |
if __name__ == "__main__":
|