Spaces:
Sleeping
Sleeping
에러 발생으로 실행 불가
Browse files- app.py +43 -9
- data/saves/simulation_state.pkl +0 -0
- npc_social_network/README.txt +5 -3
- npc_social_network/data/saves/simulation_state.pkl +0 -0
- npc_social_network/manager/simulation_manager.py +1 -1
- npc_social_network/npc/npc_emotion.py +1 -1
- npc_social_network/routes/npc_route.py +90 -68
- npc_social_network/scenarios/scenario_setup.py +4 -4
- npc_social_network/simulation_core.py +14 -12
- npc_social_network/static/images/{tiles/animal/animal_horse.png → npc/npc_다이애나.png} +2 -2
- npc_social_network/static/images/{tiles/animal/animal_caw.png → npc/npc_밥.png} +2 -2
- npc_social_network/static/images/{tiles/animal/animal_chicken.png → npc/npc_앨리스.png} +2 -2
- npc_social_network/static/images/{tiles/animal/animal_dog.png → npc/npc_엘린.png} +2 -2
- npc_social_network/static/images/npc/npc_찰리.png +3 -0
- npc_social_network/static/images/tiles/animal/animal_pig.png +0 -3
- npc_social_network/static/images/tiles/animal/animal_sheep.png +0 -3
- npc_social_network/static/images/tiles/building/build_house.png +0 -3
- npc_social_network/static/images/tiles/building/build_market.png +0 -3
- npc_social_network/static/images/tiles/building/build_temple.png +0 -3
- npc_social_network/static/images/tiles/npc/npc.png +0 -3
- npc_social_network/static/images/tiles/plant/plant_raspberry.png +0 -3
- npc_social_network/static/images/tiles/plant/plant_tree.png +0 -3
- npc_social_network/static/images/tiles/plant/plant_wheat.png +0 -3
- npc_social_network/static/images/tiles/terrain/tile_dirt.png +0 -3
- npc_social_network/static/images/tiles/terrain/tile_grass.png +0 -3
- npc_social_network/static/images/tiles/terrain/tile_pool.png +0 -3
- npc_social_network/static/images/tiles/terrain/tile_stone.png +0 -3
- npc_social_network/static/images/tiles/terrain/tile_water.png +0 -3
- npc_social_network/templates/base.html +0 -15
- npc_social_network/templates/chat.html +0 -57
- npc_social_network/templates/dashboard.html +220 -0
- templates/main.html +8 -6
app.py
CHANGED
@@ -1,18 +1,41 @@
|
|
1 |
# portfolio/app.py
|
2 |
from flask import Flask, render_template, url_for, redirect
|
3 |
-
from npc_social_network.routes.npc_route import npc_bp
|
4 |
-
from npc_social_network import simulation_core
|
5 |
# from stock.routes.stock_route import stock_bp
|
6 |
-
import
|
7 |
-
import subprocess
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
def create_app():
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
-
# 각 프로젝트는 Blueprint에서 자기 static/template 관리
|
14 |
-
app.register_blueprint(npc_bp)
|
15 |
-
# app.register_blueprint(stock_bp)
|
16 |
|
17 |
# 포트폴리오의 메인 랜딩 페이지
|
18 |
@app.route("/")
|
@@ -25,7 +48,18 @@ if __name__ == '__main__':
|
|
25 |
# 1. flask 앱 실행
|
26 |
app = create_app()
|
27 |
# 2. NPC 시뮬레이션 초기화 및 백그라운드 스레드 시작
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
# 3. Flask 웹 서버 실행
|
31 |
# use_reloader=False는 디버그 모드에서 앱이 두 번 실행되는 것을 방지하여,
|
|
|
1 |
# portfolio/app.py
|
2 |
from flask import Flask, render_template, url_for, redirect
|
3 |
+
# from npc_social_network.routes.npc_route import npc_bp
|
4 |
+
# from npc_social_network import simulation_core
|
5 |
# from stock.routes.stock_route import stock_bp
|
6 |
+
import threading
|
|
|
7 |
|
8 |
+
# -------------------------------------------------------------------
|
9 |
+
# 파일 1: portfolio/app.py (수정)
|
10 |
+
# 역할: 전체 포트폴리오의 진입점.
|
11 |
+
# NPC 시뮬레이션 스레드를 시작하고, 관련 URL 그룹(Blueprint)을 등록합니다.
|
12 |
+
# -------------------------------------------------------------------
|
13 |
+
|
14 |
+
# 시뮬레이션이 여러 번 초기화되는 것을 방지하기 위한 잠금 장치
|
15 |
+
init_lock = threading.Lock()
|
16 |
+
simulation_initialized = False
|
17 |
|
18 |
def create_app():
|
19 |
+
"""Flask 앱을 생성하고, 필요한 Blueprint를 등록하는 팩토리 함수."""
|
20 |
+
app = Flask(__name__,
|
21 |
+
template_folder='templates') # static/template 경로를 기본값
|
22 |
+
|
23 |
+
try:
|
24 |
+
from npc_social_network.routes.npc_route import npc_bp
|
25 |
+
app.register_blueprint(npc_bp)
|
26 |
+
print("✅ 'npc_social' Blueprint가 성공적으로 등록되었습니다.")
|
27 |
+
# app.register_blueprint(stock_bp)
|
28 |
+
except Exception as e:
|
29 |
+
import traceback
|
30 |
+
print("="*60)
|
31 |
+
print("❌ CRITICAL ERROR: Blueprint 등록 중 에러가 발생했습니다!")
|
32 |
+
print(f" 에러 메시지: {e}")
|
33 |
+
print("="*60)
|
34 |
+
traceback.print_exc() # 에러의 전체 경로를 출력합니다.
|
35 |
+
print("="*60)
|
36 |
+
# 이 에러가 해결될 때까지 서버는 정상 작동하지 않을 수 있습니다.
|
37 |
+
# --- FINAL DEBUGGING TOOL END ---
|
38 |
|
|
|
|
|
|
|
39 |
|
40 |
# 포트폴리오의 메인 랜딩 페이지
|
41 |
@app.route("/")
|
|
|
48 |
# 1. flask 앱 실행
|
49 |
app = create_app()
|
50 |
# 2. NPC 시뮬레이션 초기화 및 백그라운드 스레드 시작
|
51 |
+
try:
|
52 |
+
from npc_social_network import simulation_core
|
53 |
+
simulation_core.initialize_simulation()
|
54 |
+
print("✅ 시뮬레이션이 성공적으로 초기화되었습니다.")
|
55 |
+
except Exception as e:
|
56 |
+
import traceback
|
57 |
+
print("="*60)
|
58 |
+
print("❌ CRITICAL ERROR: 시뮬레이션 초기화 중 에러가 발생했습니다!")
|
59 |
+
print(f" 에러 메시지: {e}")
|
60 |
+
print("="*60)
|
61 |
+
traceback.print_exc()
|
62 |
+
print("="*60)
|
63 |
|
64 |
# 3. Flask 웹 서버 실행
|
65 |
# use_reloader=False는 디버그 모드에서 앱이 두 번 실행되는 것을 방지하여,
|
data/saves/simulation_state.pkl
DELETED
Binary file (19.7 kB)
|
|
npc_social_network/README.txt
CHANGED
@@ -24,6 +24,10 @@ portfolio/
|
|
24 |
├── README.txt
|
25 |
├── routes/
|
26 |
│ └── npc_route.py # 웹 라우트 (API)
|
|
|
|
|
|
|
|
|
27 |
├── npc/
|
28 |
│ ├── npc_base.py # NPC 본체 (감정/기억/관계 포함)
|
29 |
│ ├── npc_manager.py # NPC 리스트 관리
|
@@ -38,9 +42,7 @@ portfolio/
|
|
38 |
│ ├── llm_helper.py # 감정 추론, 응답 생성
|
39 |
│ └── llm_prompt_builder.py # 프롬프트 생성
|
40 |
├── templates/
|
41 |
-
│
|
42 |
-
│ ├── base.html # 공통 템플릿
|
43 |
-
│ └── test_memory_tools.html # 임베딩 테스트용 페이지
|
44 |
└── static/
|
45 |
├── css/style.css
|
46 |
└── js/npc_chat.js
|
|
|
24 |
├── README.txt
|
25 |
├── routes/
|
26 |
│ └── npc_route.py # 웹 라우트 (API)
|
27 |
+
├── manager/
|
28 |
+
│ └── simulation_manager.py
|
29 |
+
├── scenarios/
|
30 |
+
│ └── scenario_setup.py
|
31 |
├── npc/
|
32 |
│ ├── npc_base.py # NPC 본체 (감정/기억/관계 포함)
|
33 |
│ ├── npc_manager.py # NPC 리스트 관리
|
|
|
42 |
│ ├── llm_helper.py # 감정 추론, 응답 생성
|
43 |
│ └── llm_prompt_builder.py # 프롬프트 생성
|
44 |
├── templates/
|
45 |
+
│ └── dashboard.html # 임베딩 테스트용 페이지
|
|
|
|
|
46 |
└── static/
|
47 |
├── css/style.css
|
48 |
└── js/npc_chat.js
|
npc_social_network/data/saves/simulation_state.pkl
ADDED
Binary file (12.8 kB). View file
|
|
npc_social_network/manager/simulation_manager.py
CHANGED
@@ -3,7 +3,7 @@ import pickle
|
|
3 |
import os
|
4 |
from npc_social_network.npc.npc_manager import NPCManager
|
5 |
|
6 |
-
SAVE_DIR = "data/saves"
|
7 |
if not os.path.exists(SAVE_DIR):
|
8 |
os.makedirs(SAVE_DIR)
|
9 |
|
|
|
3 |
import os
|
4 |
from npc_social_network.npc.npc_manager import NPCManager
|
5 |
|
6 |
+
SAVE_DIR = "npc_social_network/data/saves"
|
7 |
if not os.path.exists(SAVE_DIR):
|
8 |
os.makedirs(SAVE_DIR)
|
9 |
|
npc_social_network/npc/npc_emotion.py
CHANGED
@@ -42,7 +42,7 @@ class EmotionManager:
|
|
42 |
self._emotion_buffer[emotion] = min(max(0.0, self._emotion_buffer[emotion]), 100.0)
|
43 |
|
44 |
|
45 |
-
def
|
46 |
"""
|
47 |
시간이 지남에 따라 모든 감정이 서서히 감소
|
48 |
"""
|
|
|
42 |
self._emotion_buffer[emotion] = min(max(0.0, self._emotion_buffer[emotion]), 100.0)
|
43 |
|
44 |
|
45 |
+
def decay_emotions(self):
|
46 |
"""
|
47 |
시간이 지남에 따라 모든 감정이 서서히 감소
|
48 |
"""
|
npc_social_network/routes/npc_route.py
CHANGED
@@ -1,8 +1,7 @@
|
|
1 |
# portfolio/npc_social_network/routes/npc_route.py
|
2 |
from flask import Blueprint, render_template, request, jsonify, url_for
|
|
|
3 |
from .. import simulation_core
|
4 |
-
from npc_social_network.maps.manager.simulation_manager import load_simulation, save_simulation
|
5 |
-
from ..npc.npc_manager import NPCManager
|
6 |
|
7 |
|
8 |
npc_bp = Blueprint(
|
@@ -10,81 +9,85 @@ npc_bp = Blueprint(
|
|
10 |
__name__,
|
11 |
url_prefix="/npc_social_network",
|
12 |
template_folder="../templates",
|
13 |
-
static_folder="../static"
|
|
|
14 |
|
15 |
-
# --------------------------------------------------
|
16 |
-
# API 엔드포인트 정의
|
17 |
-
# --------------------------------------------------
|
18 |
@npc_bp.route("/")
|
19 |
def dashboard():
|
20 |
-
"""월드 관찰자 대시보드 페이지를 렌더링합니다."""
|
21 |
return render_template("dashboard.html")
|
22 |
|
23 |
-
@npc_bp.route("api/world_state",
|
24 |
def get_world_state():
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
nodes.append({
|
32 |
-
"id": npc.name,
|
33 |
-
"label": npc.name,
|
34 |
-
"shape": "image",
|
35 |
-
"image": url_for('npc_social.static', filename=f'image/npc/npc{npc.name.lower()}.png'),
|
36 |
-
"job": npc.job,
|
37 |
-
"age": npc.age
|
38 |
-
})
|
39 |
-
|
40 |
-
edges = []
|
41 |
-
drawn_relations = set()
|
42 |
-
for npc in simulation_core.npc_manager.all():
|
43 |
-
for target_name, profile in npc.relationships.relationships.items():
|
44 |
-
relation_pair = tuple(sorted(npc.name, target_name))
|
45 |
-
if relation_pair in drawn_relations: continue
|
46 |
-
|
47 |
-
color = {"best friend": "#0400FF", "friend": "#00FF00", "acquaintance": "#68D4FF",
|
48 |
-
"nuisance": "#FCF814", "rival": "#FF9E0D", "enemy": "#FF0000",
|
49 |
-
"stranger":"#5E5C5C" }.get(profile.type, "gray")
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
57 |
})
|
58 |
-
drawn_relations.add(relation_pair)
|
59 |
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
-
|
74 |
-
|
75 |
-
"
|
76 |
-
"
|
77 |
-
|
78 |
-
"
|
79 |
-
|
80 |
-
|
|
|
81 |
|
82 |
@npc_bp.route("/api/toggle_simulation", methods=['POST'])
|
83 |
def toggle_simulation():
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
|
|
|
|
88 |
|
89 |
@npc_bp.route("/api/manual_tick", methods=['POST'])
|
90 |
def manual_tick():
|
@@ -103,8 +106,27 @@ def inject_event():
|
|
103 |
npc_name, event_text = data.get("npc_name"), data.get("event_text")
|
104 |
npc = simulation_core.npc_manager.get_npc_by_name(npc_name)
|
105 |
|
106 |
-
|
107 |
-
npc
|
108 |
-
|
109 |
-
|
110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# portfolio/npc_social_network/routes/npc_route.py
|
2 |
from flask import Blueprint, render_template, request, jsonify, url_for
|
3 |
+
import traceback
|
4 |
from .. import simulation_core
|
|
|
|
|
5 |
|
6 |
|
7 |
npc_bp = Blueprint(
|
|
|
9 |
__name__,
|
10 |
url_prefix="/npc_social_network",
|
11 |
template_folder="../templates",
|
12 |
+
static_folder="../static"
|
13 |
+
)
|
14 |
|
|
|
|
|
|
|
15 |
@npc_bp.route("/")
|
16 |
def dashboard():
|
|
|
17 |
return render_template("dashboard.html")
|
18 |
|
19 |
+
@npc_bp.route("/api/world_state", methods=['GET'])
|
20 |
def get_world_state():
|
21 |
+
# --- DEBUGGING TOOL START ---
|
22 |
+
# 이 try...except 블록이 에러의 상세 내용을 터미널에 출력해줍니다.
|
23 |
+
try:
|
24 |
+
with simulation_core.simulation_lock:
|
25 |
+
if not simulation_core.npc_manager:
|
26 |
+
return jsonify({"error": "Simulation not started"}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
+
# 데이터 복사 (잠금 시간을 최소화하기 위해)
|
29 |
+
all_npcs = list(simulation_core.npc_manager.all())
|
30 |
+
current_log = list(simulation_core.event_log)
|
31 |
+
is_paused = simulation_core.simulation_paused
|
32 |
+
|
33 |
+
nodes = []
|
34 |
+
for npc in all_npcs:
|
35 |
+
nodes.append({
|
36 |
+
"id": npc.name,
|
37 |
+
"label": npc.name,
|
38 |
+
"shape": "image",
|
39 |
+
"image": url_for('npc_social.static', filename=f'images/npc/npc_{npc.name.lower()}.png'),
|
40 |
+
"job": npc.job,
|
41 |
+
"age": npc.age
|
42 |
})
|
|
|
43 |
|
44 |
+
edges = []
|
45 |
+
drawn_relations = set()
|
46 |
+
for npc in all_npcs:
|
47 |
+
for target_name, profile in npc.relationships.relationships.items():
|
48 |
+
relation_pair = tuple(sorted((npc.name, target_name)))
|
49 |
+
if relation_pair in drawn_relations: continue
|
50 |
|
51 |
+
color = {
|
52 |
+
"best friend": "#0400FF", "friend": "#00FF00", "acquaintance": "#68D4FF",
|
53 |
+
"nuisance": "#FCF814", "rival": "#FF9E0D", "enemy": "#FF0000",
|
54 |
+
"stranger":"#5E5C5C"
|
55 |
+
}.get(profile.type, "gray")
|
56 |
+
|
57 |
+
edges.append({
|
58 |
+
"from": npc.name,
|
59 |
+
"to": target_name,
|
60 |
+
"label": f"{profile.type} ({profile.score:.1f})",
|
61 |
+
"color": color,
|
62 |
+
"width": 2
|
63 |
+
})
|
64 |
+
drawn_relations.add(relation_pair)
|
65 |
+
|
66 |
+
return jsonify({
|
67 |
+
"nodes": nodes,
|
68 |
+
"edges": edges,
|
69 |
+
"log": current_log,
|
70 |
+
"paused": is_paused
|
71 |
+
})
|
72 |
|
73 |
+
except Exception as e:
|
74 |
+
# 에러 발생 시, 터미널에 상세한 로그를 출력합니다.
|
75 |
+
print("="*40)
|
76 |
+
print(f"!!! ERROR in get_world_state: {e}")
|
77 |
+
traceback.print_exc() # 에러가 발생한 위치를 정확히 알려줍니다.
|
78 |
+
print("="*40)
|
79 |
+
# 프론트엔드에도 에러가 발생했음을 알립니다.
|
80 |
+
return jsonify({"error": "An internal error occurred. Check the server terminal for details."}), 500
|
81 |
+
# --- DEBUGGING TOOL END ---
|
82 |
|
83 |
@npc_bp.route("/api/toggle_simulation", methods=['POST'])
|
84 |
def toggle_simulation():
|
85 |
+
with simulation_core.simulation_lock:
|
86 |
+
simulation_core.simulation_paused = not simulation_core.simulation_paused
|
87 |
+
status = "정지됨" if simulation_core.simulation_paused else "실행 중"
|
88 |
+
simulation_core.add_log(f"시뮬레이션이 {status} 상태로 변경되었습니다.")
|
89 |
+
is_paused = simulation_core.simulation_paused
|
90 |
+
return jsonify({"paused": is_paused})
|
91 |
|
92 |
@npc_bp.route("/api/manual_tick", methods=['POST'])
|
93 |
def manual_tick():
|
|
|
106 |
npc_name, event_text = data.get("npc_name"), data.get("event_text")
|
107 |
npc = simulation_core.npc_manager.get_npc_by_name(npc_name)
|
108 |
|
109 |
+
with simulation_core.simulation_lock:
|
110 |
+
npc = simulation_core.npc_manager.get_npc_by_name(npc_name)
|
111 |
+
if npc and event_text:
|
112 |
+
npc.remember(content=f"[요약된 기억] {event_text}", importance=10, emotion="surprise", memory_type="Summary") # 수정 필요: 신이 돼서 넣는게 아니라, NPC가 마치 과거에 겪은 일처럼 자연스럽게 느끼도록
|
113 |
+
simulation_core.add_log(f"이벤트 주입(기억 요약) -> {npc_name}: '{event_text}'")
|
114 |
+
return jsonify({"success": True})
|
115 |
+
return jsonify({"success": False, "error": "Invalid data"}), 400
|
116 |
+
|
117 |
+
# get_npc_details 와 같은 나머지 읽기 전용 API도 Lock을 추가하면 더 안전합니다.
|
118 |
+
@npc_bp.route("/api/npc_details/<npc_name>", methods=['GET'])
|
119 |
+
def get_npc_details(npc_name):
|
120 |
+
with simulation_core.simulation_lock:
|
121 |
+
npc = simulation_core.npc_manager.get_npc_by_name(npc_name)
|
122 |
+
if not npc:
|
123 |
+
return jsonify({"error": "NPC not found"}), 404
|
124 |
+
|
125 |
+
details = {
|
126 |
+
"name": npc.name, "age": npc.age, "job": npc.job,
|
127 |
+
"personality_summary": npc.personality.get_personality_summary(),
|
128 |
+
"emotions": npc.get_composite_emotion_state(top_n=5),
|
129 |
+
"goals": npc.planner.current_goal.description if npc.planner.has_active_plan() else "특별한 목표 없음",
|
130 |
+
"memories": [mem.content for mem in npc.memory_store.get_recent_memories(limit=10)]
|
131 |
+
}
|
132 |
+
return jsonify(details)
|
npc_social_network/scenarios/scenario_setup.py
CHANGED
@@ -2,7 +2,7 @@ from ..npc.npc_manager import NPCManager
|
|
2 |
from ..npc.npc_base import NPC
|
3 |
from ..npc.npc_memory import Memory
|
4 |
|
5 |
-
def setup_initial_scenario(
|
6 |
"""
|
7 |
테스트를 위한 초기 NPC 월드를 설정하고 NPCManager를 반환합니다.
|
8 |
- 5명의 NPC 생성
|
@@ -23,7 +23,7 @@ def setup_initial_scenario(image_loader) -> NPCManager:
|
|
23 |
|
24 |
# 앨리스: 사교적이고 계산적인 상인
|
25 |
personality_alice = {"sensitive": 0.5, "stoic": 0.5, "cognitive_bias": 0.8}
|
26 |
-
alice = NPC("앨리스", "
|
27 |
|
28 |
# 찰리: 성실하고 평화로운 농부
|
29 |
personality_charlie = {"sensitive": 0.6, "stoic": 0.6, "cognitive_bias": 0.3}
|
@@ -35,7 +35,7 @@ def setup_initial_scenario(image_loader) -> NPCManager:
|
|
35 |
|
36 |
# --- 2. 초기 기억 주입 ---
|
37 |
elin.remember(content="어젯밤 앨리스와 시장 가격 때문에 크게 다퉜다.", importance=8, emotion="anger")
|
38 |
-
alice.remember(content="어젯밤
|
39 |
bob.remember(content="찰리가 어제 우리 집 지붕을 고쳐주어서 고맙다.", importance=7, emotion="gratitude")
|
40 |
charlie.remember(content="밥의 대장간 일을 도와주고 빵을 얻었다. 그는 좋은 친구다.", importance=6, emotion="joy")
|
41 |
diana.remember(content="도서관에서 밥이 책을 빌려가며 거칠게 다루어 조금 기분이 상했다.", importance=5, emotion="disgust")
|
@@ -50,7 +50,7 @@ def setup_initial_scenario(image_loader) -> NPCManager:
|
|
50 |
# --- 4. 초기 관계 설정 ---
|
51 |
# 엘라 <-> 앨리스 (나쁜 관계)
|
52 |
elin.relationships.update_relationship("앨리스", "anger", strength=5.0)
|
53 |
-
alice.relationships.update_relationship("
|
54 |
|
55 |
# 밥 <-> 찰리 (좋은 관계)
|
56 |
bob.relationships.update_relationship("찰리", "gratitude", strength=6.0)
|
|
|
2 |
from ..npc.npc_base import NPC
|
3 |
from ..npc.npc_memory import Memory
|
4 |
|
5 |
+
def setup_initial_scenario() -> NPCManager:
|
6 |
"""
|
7 |
테스트를 위한 초기 NPC 월드를 설정하고 NPCManager를 반환합니다.
|
8 |
- 5명의 NPC 생성
|
|
|
23 |
|
24 |
# 앨리스: 사교적이고 계산적인 상인
|
25 |
personality_alice = {"sensitive": 0.5, "stoic": 0.5, "cognitive_bias": 0.8}
|
26 |
+
alice = NPC("앨리스", "상인", personality=personality_alice)
|
27 |
|
28 |
# 찰리: 성실하고 평화로운 농부
|
29 |
personality_charlie = {"sensitive": 0.6, "stoic": 0.6, "cognitive_bias": 0.3}
|
|
|
35 |
|
36 |
# --- 2. 초기 기억 주입 ---
|
37 |
elin.remember(content="어젯밤 앨리스와 시장 가격 때문에 크게 다퉜다.", importance=8, emotion="anger")
|
38 |
+
alice.remember(content="어젯밤 엘린이 내게 무례하게 소리쳤다.", importance=8, emotion="resentment")
|
39 |
bob.remember(content="찰리가 어제 우리 집 지붕을 고쳐주어서 고맙다.", importance=7, emotion="gratitude")
|
40 |
charlie.remember(content="밥의 대장간 일을 도와주고 빵을 얻었다. 그는 좋은 친구다.", importance=6, emotion="joy")
|
41 |
diana.remember(content="도서관에서 밥이 책을 빌려가며 거칠게 다루어 조금 기분이 상했다.", importance=5, emotion="disgust")
|
|
|
50 |
# --- 4. 초기 관계 설정 ---
|
51 |
# 엘라 <-> 앨리스 (나쁜 관계)
|
52 |
elin.relationships.update_relationship("앨리스", "anger", strength=5.0)
|
53 |
+
alice.relationships.update_relationship("엘린", "resentment", strength=4.0)
|
54 |
|
55 |
# 밥 <-> 찰리 (좋은 관계)
|
56 |
bob.relationships.update_relationship("찰리", "gratitude", strength=6.0)
|
npc_social_network/simulation_core.py
CHANGED
@@ -14,9 +14,10 @@ from .manager.simulation_manager import save_simulation, load_simulation
|
|
14 |
npc_manager: NPCManager = None
|
15 |
simulation_paused = True
|
16 |
event_log = []
|
|
|
17 |
|
18 |
#------------------------------------------
|
19 |
-
# 2. 시뮬레이션
|
20 |
#------------------------------------------
|
21 |
def add_log(message):
|
22 |
"""이벤트 로그에 메시지를 추가합니다."""
|
@@ -31,19 +32,20 @@ def tick_simulation():
|
|
31 |
if not npc_manager or simulation_paused:
|
32 |
return
|
33 |
|
34 |
-
|
|
|
35 |
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
|
48 |
def simulation_loop():
|
49 |
"""백그라운드에서 주기적으로 시뮬레이션을 실행하는 루프"""
|
|
|
14 |
npc_manager: NPCManager = None
|
15 |
simulation_paused = True
|
16 |
event_log = []
|
17 |
+
simulation_lock = threading.Lock() # 데이터 동시 접근을 막는 잠금장치
|
18 |
|
19 |
#------------------------------------------
|
20 |
+
# 2. 시뮬레이션 로직 함수
|
21 |
#------------------------------------------
|
22 |
def add_log(message):
|
23 |
"""이벤트 로그에 메시지를 추가합니다."""
|
|
|
32 |
if not npc_manager or simulation_paused:
|
33 |
return
|
34 |
|
35 |
+
with simulation_lock:
|
36 |
+
add_log("시뮬레이션 틱 시작")
|
37 |
|
38 |
+
for npc in npc_manager.all():
|
39 |
+
npc.decay_emotions()
|
40 |
+
npc.decay_memories()
|
41 |
+
npc.update_autonomous_behavior("자율 행동 시간")
|
42 |
|
43 |
+
if len(npc_manager.all()) >= 2:
|
44 |
+
try:
|
45 |
+
add_log("NPC 간 자율 상호작용을 시도합니다.")
|
46 |
+
npc_manager.initiate_npc_to_npc_interaction("자율 행동 시간")
|
47 |
+
except Exception as e:
|
48 |
+
add_log(f"상호작용 중 오류 발생: {e}")
|
49 |
|
50 |
def simulation_loop():
|
51 |
"""백그라운드에서 주기적으로 시뮬레이션을 실행하는 루프"""
|
npc_social_network/static/images/{tiles/animal/animal_horse.png → npc/npc_다이애나.png}
RENAMED
File without changes
|
npc_social_network/static/images/{tiles/animal/animal_caw.png → npc/npc_밥.png}
RENAMED
File without changes
|
npc_social_network/static/images/{tiles/animal/animal_chicken.png → npc/npc_앨리스.png}
RENAMED
File without changes
|
npc_social_network/static/images/{tiles/animal/animal_dog.png → npc/npc_엘린.png}
RENAMED
File without changes
|
npc_social_network/static/images/npc/npc_찰리.png
ADDED
![]() |
Git LFS Details
|
npc_social_network/static/images/tiles/animal/animal_pig.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/animal/animal_sheep.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/building/build_house.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/building/build_market.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/building/build_temple.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/npc/npc.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/plant/plant_raspberry.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/plant/plant_tree.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/plant/plant_wheat.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/terrain/tile_dirt.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/terrain/tile_grass.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/terrain/tile_pool.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/terrain/tile_stone.png
DELETED
Git LFS Details
|
npc_social_network/static/images/tiles/terrain/tile_water.png
DELETED
Git LFS Details
|
npc_social_network/templates/base.html
DELETED
@@ -1,15 +0,0 @@
|
|
1 |
-
<!-- portfolio/npc_social_network/templates/base.html -->
|
2 |
-
<!DOCTYPE html>
|
3 |
-
<html lang="en">
|
4 |
-
<head>
|
5 |
-
<meta charset="UTF-8">
|
6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
-
<title>
|
8 |
-
Document
|
9 |
-
</title>
|
10 |
-
</head>
|
11 |
-
<body>
|
12 |
-
<!-- 자바스크립트는 로딩 -->
|
13 |
-
<script src="{{ url_for('npc_social.static', filename='js/npc_chat.js') }}"></script>
|
14 |
-
</body>
|
15 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
npc_social_network/templates/chat.html
DELETED
@@ -1,57 +0,0 @@
|
|
1 |
-
<!-- portfolio/npc_social_network/templates/chat.html -->
|
2 |
-
<!DOCTYPE html>
|
3 |
-
<html lang="ko">
|
4 |
-
<head>
|
5 |
-
<meta charset="UTF-8">
|
6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
-
<title>NPC Chat</title>
|
8 |
-
|
9 |
-
<link rel="stylesheet" herf="{{ url_for('npc_social.static', filename='css/style.css') }}">
|
10 |
-
</head>
|
11 |
-
<body>
|
12 |
-
<h1>NPC 소셜 네트워크 - 통합 대시보드</h1>
|
13 |
-
|
14 |
-
<!-- 시뮬레이션 제어 버튼 -->
|
15 |
-
<div class="controls">
|
16 |
-
<a href="{{ url_for('run_npc_simulation') }}"><button>시뮬레이션 시작 / 재시작</button></a>
|
17 |
-
<p>시뮬레이션을 시작하면 별도의 Pygame 창이 열립니다.</p>
|
18 |
-
</div>
|
19 |
-
|
20 |
-
<h2>NPC와 대화하기</h2>
|
21 |
-
<div class="meta-info">
|
22 |
-
<select id="npc" onchange="loadNPCInfo()">
|
23 |
-
<!-- [MOD] 서버에서 전달받은 NPC 이름으로 동적 생성 -->
|
24 |
-
{% for name in npc_names %}
|
25 |
-
<option value="{{ name }}">{{ name }}</option>
|
26 |
-
{% else %}
|
27 |
-
<option disabled>NPC 없음</option>
|
28 |
-
{% endfor %}
|
29 |
-
</select>
|
30 |
-
<span id="relationStatus" style="margin-left:20px";>관계 점수: 로딩 중...</span>
|
31 |
-
</div>
|
32 |
-
|
33 |
-
<div id="chatBox"></div>
|
34 |
-
<input type="text" id="message" placeholder="메세지를 입력하세요" style="width:80%">
|
35 |
-
<button onclick="sendMessage()">보내기</button>
|
36 |
-
|
37 |
-
<!-- Memory 영역 추가 -->
|
38 |
-
<div class="meta-info">
|
39 |
-
<h4>Memory:</h4>
|
40 |
-
<ul id="memoryList">
|
41 |
-
<li>로딩 중...</li>
|
42 |
-
</ul>
|
43 |
-
</div>
|
44 |
-
|
45 |
-
<!-- Personality 상태 UI 표시 -->
|
46 |
-
<div class="meta-info">
|
47 |
-
<h4>Personality 상태:</h4>
|
48 |
-
<ul id="personalityStatus">
|
49 |
-
<li>로딩 중...</li>
|
50 |
-
</ul>
|
51 |
-
</div>
|
52 |
-
|
53 |
-
<a href="{{ url_for('index') }}">Back to Main Page</a>
|
54 |
-
|
55 |
-
<script src="{{ url_for('npc_social.static', filename='js/npc_chat.js') }}"></script>
|
56 |
-
</body>
|
57 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
npc_social_network/templates/dashboard.html
ADDED
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="ko">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>NPC 소셜 네트워크 대시보드</title>
|
7 |
+
<!-- vis-network 라이브러리 (CDN) -->
|
8 |
+
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
9 |
+
<style>
|
10 |
+
:root {
|
11 |
+
--bg-color: #f0f2f5;
|
12 |
+
--panel-bg-color: #ffffff;
|
13 |
+
--border-color: #dee2e6;
|
14 |
+
--shadow: 0 4px 6px rgba(0,0,0,0.05);
|
15 |
+
--primary-color: #007bff;
|
16 |
+
--success-color: #28a745;
|
17 |
+
}
|
18 |
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; display: flex; height: 100vh; margin: 0; background-color: var(--bg-color); }
|
19 |
+
#main-container { display: flex; flex: 1; padding: 15px; gap: 15px; }
|
20 |
+
#network-panel { flex: 3; display: flex; flex-direction: column; background: var(--panel-bg-color); border-radius: 12px; box-shadow: var(--shadow); }
|
21 |
+
#network { width: 100%; flex: 1; border-bottom: 1px solid var(--border-color); }
|
22 |
+
#controls { padding: 12px 15px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; background-color: #f8f9fa; border-radius: 0 0 12px 12px;}
|
23 |
+
#controls button { padding: 8px 15px; border-radius: 6px; border: none; cursor: pointer; color: white; font-size: 14px; font-weight: 500; transition: background-color 0.2s; }
|
24 |
+
#play-pause-btn { background-color: var(--success-color); }
|
25 |
+
#play-pause-btn.paused { background-color: var(--primary-color); }
|
26 |
+
#tick-btn { background-color: #6c757d; }
|
27 |
+
#inject-event-btn { background-color: #17a2b8; }
|
28 |
+
#controls select, #controls input { padding: 8px; border-radius: 6px; border: 1px solid var(--border-color); flex-grow: 1; }
|
29 |
+
|
30 |
+
#side-panel { flex: 1; display: flex; flex-direction: column; gap: 15px; min-width: 300px; }
|
31 |
+
.info-panel { background: var(--panel-bg-color); padding: 20px; border-radius: 12px; box-shadow: var(--shadow); overflow-y: auto; }
|
32 |
+
#npc-details { flex-basis: 45%; }
|
33 |
+
#log-panel { flex-basis: 55%; display: flex; flex-direction: column; }
|
34 |
+
#log-container { flex: 1; overflow-y: auto; font-size: 13px; line-height: 1.6; color: #495057; white-space: pre-wrap; word-wrap: break-word; }
|
35 |
+
h2 { margin-top: 0; margin-bottom: 15px; font-size: 18px; color: #343a40; border-bottom: 1px solid var(--border-color); padding-bottom: 10px; }
|
36 |
+
ul { padding-left: 20px; margin: 0; }
|
37 |
+
li { margin-bottom: 8px; }
|
38 |
+
</style>
|
39 |
+
</head>
|
40 |
+
<body>
|
41 |
+
<div id="main-container">
|
42 |
+
<!-- 관계 네트워크 패널 -->
|
43 |
+
<div id="network-panel">
|
44 |
+
<div id="network"></div>
|
45 |
+
<div id="controls">
|
46 |
+
<button id="play-pause-btn">Play</button>
|
47 |
+
<button id="tick-btn">Next Tick</button>
|
48 |
+
<select id="event-npc-select"></select>
|
49 |
+
<input type="text" id="event-text-input" placeholder="주입할 이벤트 입력 (예: 길에서 금화를 주웠다)">
|
50 |
+
<button id="inject-event-btn">이벤트 주입</button>
|
51 |
+
</div>
|
52 |
+
</div>
|
53 |
+
|
54 |
+
<!-- 사이드 정보 패널 -->
|
55 |
+
<div id="side-panel">
|
56 |
+
<div id="npc-details" class="info-panel">
|
57 |
+
<h2>NPC 상세 정보</h2>
|
58 |
+
<div id="npc-details-content">관계도에서 NPC를 선택하세요.</div>
|
59 |
+
</div>
|
60 |
+
<div id="log-panel" class="info-panel">
|
61 |
+
<h2>실시간 이벤트 로그</h2>
|
62 |
+
<div id="log-container">(로그 로딩 중...)</div>
|
63 |
+
</div>
|
64 |
+
</div>
|
65 |
+
</div>
|
66 |
+
|
67 |
+
<script>
|
68 |
+
document.addEventListener('DOMContentLoaded', function () {
|
69 |
+
const networkContainer = document.getElementById('network');
|
70 |
+
const logContainer = document.getElementById('log-container');
|
71 |
+
const detailsContainer = document.getElementById('npc-details-content');
|
72 |
+
const playPauseBtn = document.getElementById('play-pause-btn');
|
73 |
+
const tickBtn = document.getElementById('tick-btn');
|
74 |
+
const injectEventBtn = document.getElementById('inject-event-btn');
|
75 |
+
const eventNpcSelect = document.getElementById('event-npc-select');
|
76 |
+
const eventTextInput = document.getElementById('event-text-input');
|
77 |
+
|
78 |
+
let network = null;
|
79 |
+
let nodes = new vis.DataSet([]);
|
80 |
+
let edges = new vis.DataSet([]);
|
81 |
+
let autoUpdateInterval = null;
|
82 |
+
|
83 |
+
const options = {
|
84 |
+
nodes: {
|
85 |
+
borderWidth: 3,
|
86 |
+
size: 30,
|
87 |
+
color: { border: '#222222', background: '#666666' },
|
88 |
+
font: { color: '#000000' }
|
89 |
+
},
|
90 |
+
edges: {
|
91 |
+
font: { align: 'top' },
|
92 |
+
smooth: { type: 'cubicBezier' }
|
93 |
+
},
|
94 |
+
physics: {
|
95 |
+
stabilization: false,
|
96 |
+
solver: 'forceAtlas2Based',
|
97 |
+
forceAtlas2Based: { gravitationalConstant: -50, springLength: 100, springConstant: 0.08 }
|
98 |
+
},
|
99 |
+
interaction: { hover: true, tooltipDelay: 200 }
|
100 |
+
};
|
101 |
+
|
102 |
+
async function updateWorld() {
|
103 |
+
try {
|
104 |
+
const response = await fetch('/npc_social_network/api/world_state');
|
105 |
+
const data = await response.json();
|
106 |
+
|
107 |
+
if (data.error) throw new Error(data.error);
|
108 |
+
|
109 |
+
// vis.js 데이터셋 업데이트
|
110 |
+
nodes.update(data.nodes);
|
111 |
+
edges.update(data.edges);
|
112 |
+
|
113 |
+
// 네트워크가 없으면 새로 생성
|
114 |
+
if (!network) {
|
115 |
+
network = new vis.Network(networkContainer, { nodes, edges }, options);
|
116 |
+
network.on('click', onNodeClick);
|
117 |
+
}
|
118 |
+
|
119 |
+
updateNpcSelect(data.nodes);
|
120 |
+
updateLog(data.log);
|
121 |
+
updatePlayPauseButton(data.paused);
|
122 |
+
|
123 |
+
} catch (error) {
|
124 |
+
console.error('Error fetching world state:', error);
|
125 |
+
logContainer.textContent = "서버 연결 오류. app.py를 실행했는지 확인해주세요.";
|
126 |
+
}
|
127 |
+
}
|
128 |
+
|
129 |
+
async function onNodeClick(params) {
|
130 |
+
if (params.nodes.length > 0) {
|
131 |
+
const npcName = params.nodes[0];
|
132 |
+
try {
|
133 |
+
const response = await fetch(`/npc_social_network/api/npc_details/${npcName}`);
|
134 |
+
const details = await response.json();
|
135 |
+
|
136 |
+
let detailsHtml = `<h3>${details.name} (${details.job}, ${details.age}세)</h3>`;
|
137 |
+
detailsHtml += `<p><strong>성격:</strong> ${details.personality_summary}</p>`;
|
138 |
+
detailsHtml += `<p><strong>목표:</strong> ${details.goals}</p>`;
|
139 |
+
detailsHtml += `<strong>감정:</strong><ul>${details.emotions.length > 0 ? details.emotions.map(e => `<li>${e[0]}: ${e[1].toFixed(1)}</li>`).join('') : '<li>평온함</li>'}</ul>`;
|
140 |
+
detailsHtml += `<strong>최근 기억 (10개):</strong><ul>${details.memories.length > 0 ? details.memories.map(m => `<li>${m.substring(0, 80)}...</li>`).join('') : '<li>기억 없음</li>'}</ul>`;
|
141 |
+
|
142 |
+
detailsContainer.innerHTML = detailsHtml;
|
143 |
+
|
144 |
+
} catch (error) {
|
145 |
+
detailsContainer.innerHTML = `${npcName}의 정보를 불러오는 데 실패했습니다.`;
|
146 |
+
}
|
147 |
+
}
|
148 |
+
}
|
149 |
+
|
150 |
+
function updateNpcSelect(npcNodes) {
|
151 |
+
const currentSelection = eventNpcSelect.value;
|
152 |
+
eventNpcSelect.innerHTML = '';
|
153 |
+
npcNodes.forEach(node => {
|
154 |
+
const option = document.createElement('option');
|
155 |
+
option.value = node.id;
|
156 |
+
option.textContent = node.id;
|
157 |
+
eventNpcSelect.appendChild(option);
|
158 |
+
});
|
159 |
+
if (currentSelection) eventNpcSelect.value = currentSelection;
|
160 |
+
}
|
161 |
+
|
162 |
+
function updateLog(logMessages) {
|
163 |
+
logContainer.textContent = logMessages.join('\n');
|
164 |
+
}
|
165 |
+
|
166 |
+
function updatePlayPauseButton(isPaused) {
|
167 |
+
playPauseBtn.textContent = isPaused ? '▶ Play' : '❚❚ Pause';
|
168 |
+
if (isPaused) {
|
169 |
+
playPauseBtn.textContent = '▶ Play';
|
170 |
+
playPauseBtn.classList.remove('paused');
|
171 |
+
if (autoUpdateInterval) {
|
172 |
+
clearInterval(autoUpdateInterval);
|
173 |
+
autoUpdateInterval = null;
|
174 |
+
}
|
175 |
+
} else {
|
176 |
+
playPauseBtn.textContent = '❚❚ Pause';
|
177 |
+
playPauseBtn.classList.add('paused');
|
178 |
+
if (!autoUpdateInterval) {
|
179 |
+
// 시뮬레이션이 실행 중일 때만 자동 업데이트
|
180 |
+
autoUpdateInterval = setInterval(updateWorld, 5000);
|
181 |
+
}
|
182 |
+
}
|
183 |
+
}
|
184 |
+
|
185 |
+
// --- 이벤트 리스너 ---
|
186 |
+
playPauseBtn.addEventListener('click', async () => {
|
187 |
+
const response = await fetch('/npc_social_network/api/toggle_simulation', { method: 'POST' });
|
188 |
+
const data = await response.json();
|
189 |
+
updatePlayPauseButton(data.paused);
|
190 |
+
// 상태 변경 후 즉시 UI 업데이트
|
191 |
+
updateWorld();
|
192 |
+
});
|
193 |
+
|
194 |
+
tickBtn.addEventListener('click', async () => {
|
195 |
+
logContainer.textContent = "수동 틱 실행 중...";
|
196 |
+
await fetch('/npc_social_network/api/manual_tick', { method: 'POST' });
|
197 |
+
await updateWorld();
|
198 |
+
});
|
199 |
+
|
200 |
+
injectEventBtn.addEventListener('click', async () => {
|
201 |
+
const npc_name = eventNpcSelect.value;
|
202 |
+
const event_text = eventTextInput.value;
|
203 |
+
if (!npc_name || !event_text) return alert('NPC와 이벤트 내용을 모두 입력해주세요.');
|
204 |
+
|
205 |
+
await fetch('/npc_social_network/api/inject_event', {
|
206 |
+
method: 'POST',
|
207 |
+
headers: { 'Content-Type': 'application/json' },
|
208 |
+
body: JSON.stringify({ npc_name, event_text })
|
209 |
+
});
|
210 |
+
|
211 |
+
eventTextInput.value = '';
|
212 |
+
await updateWorld();
|
213 |
+
});
|
214 |
+
|
215 |
+
// --- 초기화 ---
|
216 |
+
updateWorld();
|
217 |
+
});
|
218 |
+
</script>
|
219 |
+
</body>
|
220 |
+
</html>
|
templates/main.html
CHANGED
@@ -1,15 +1,17 @@
|
|
1 |
<!--portfolio/templates/main.html-->
|
2 |
<!DOCTYPE html>
|
3 |
-
<html lang="
|
4 |
<head>
|
5 |
<meta charset="UTF-8">
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
-
<title
|
8 |
</head>
|
9 |
<body>
|
10 |
-
<h1
|
11 |
-
<
|
12 |
-
|
13 |
-
|
|
|
|
|
14 |
</body>
|
15 |
</html>
|
|
|
1 |
<!--portfolio/templates/main.html-->
|
2 |
<!DOCTYPE html>
|
3 |
+
<html lang="ko">
|
4 |
<head>
|
5 |
<meta charset="UTF-8">
|
6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
+
<title>포트폴리오</title>
|
8 |
</head>
|
9 |
<body>
|
10 |
+
<h1>포트폴리오 메인</h1>
|
11 |
+
<p>프로젝트 목록:</p>
|
12 |
+
<ul>
|
13 |
+
<!-- BUG FIX: form 대신 간단한 링크로 수정 -->
|
14 |
+
<li><a href="{{ url_for('npc_social.dashboard') }}">NPC 소셜 네트워크 관찰자 대시보드</a></li>
|
15 |
+
</ul>
|
16 |
</body>
|
17 |
</html>
|