Skip to content

Commit 67dd995

Browse files
committed
archdraw agent및 힌트부분 수정
archdraw agent및 힌트부분 수정
1 parent f4372bf commit 67dd995

File tree

7 files changed

+255
-32
lines changed

7 files changed

+255
-32
lines changed

backend/core/services/wars/agents/chaos/nodes.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,15 +395,27 @@ def regenerate(state: ChaosAgentState) -> ChaosAgentState:
395395
# ─────────────────────────────────────────────────────────────────────────────
396396

397397
def finalize(state: ChaosAgentState) -> ChaosAgentState:
398-
"""최종 이벤트 확정"""
398+
"""최종 이벤트 확정 + required_component 결정 (채점 반영용)"""
399399
logger.info("[ChaosAgent] ▶ finalize 노드 실행")
400400

401401
event = state.get("raw_event")
402402
if not event:
403403
logger.error("[ChaosAgent] 최종 이벤트 없음 → 폴백")
404404
event = _fallback_by_nodes(state["deployed_nodes"])
405405

406-
logger.info(f"[ChaosAgent] ✅ 이벤트 확정: {event.get('event_id')} / severity={event.get('severity')}")
406+
# ✅ [Agent 행동] required_component 결정
407+
# target_components 중 아직 배치 안 된 것 → 플레이어가 추가해야 할 컴포넌트
408+
# 이미 다 배치한 경우 → target 중 하나를 그대로 사용 (심화 조건)
409+
deployed_set = set(state.get("deployed_nodes", []))
410+
targets = event.get("target_components", [])
411+
not_deployed = [c for c in targets if c not in deployed_set]
412+
required_component = not_deployed[0] if not_deployed else (targets[0] if targets else None)
413+
event["required_component"] = required_component
414+
415+
logger.info(
416+
f"[ChaosAgent] ✅ 이벤트 확정: {event.get('event_id')} / "
417+
f"severity={event.get('severity')} / required_component={required_component}"
418+
)
407419
return {**state, "final_event": event}
408420

409421

backend/core/services/wars/agents/coach/nodes.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,12 @@ def generate_hint(state: CoachAgentState) -> CoachAgentState:
183183

184184

185185
def _generate_direct_hint(situation: str, missing: list, state: CoachAgentState) -> Dict:
186-
"""룰 기반 직접 힌트"""
186+
"""룰 기반 직접 힌트 + highlight_component 포함"""
187187
if situation == "empty":
188188
return {
189189
"message": "💡 아직 아무것도 배치하지 않으셨네요. 사용자(Client)부터 시작해보는 건 어떨까요?",
190190
"missing_components": list(state["mission_required"]),
191+
"highlight_component": "client", # ✅ [Agent 행동] 팔레트 하이라이트 대상
191192
"type": "general",
192193
"level": 1,
193194
}
@@ -203,6 +204,7 @@ def _generate_direct_hint(situation: str, missing: list, state: CoachAgentState)
203204
return {
204205
"message": f"💡 {msg}",
205206
"missing_components": missing,
207+
"highlight_component": priority, # ✅ [Agent 행동] 누락된 컴포넌트 팔레트 하이라이트
206208
"type": "missing_component",
207209
"level": 1,
208210
}
@@ -211,11 +213,12 @@ def _generate_direct_hint(situation: str, missing: list, state: CoachAgentState)
211213
return {
212214
"message": "💡 컴포넌트를 모두 배치했네요! 이제 데이터 흐름(화살표)을 연결해보세요.",
213215
"missing_components": [],
216+
"highlight_component": None,
214217
"type": "no_arrows",
215218
"level": 1,
216219
}
217220

218-
return {"message": "", "missing_components": [], "type": "complete", "level": 0}
221+
return {"message": "", "missing_components": [], "highlight_component": None, "type": "complete", "level": 0}
219222

220223

221224
def _generate_socratic_hint(state: CoachAgentState, missing: list) -> Dict:
@@ -252,9 +255,17 @@ def _generate_socratic_hint(state: CoachAgentState, missing: list) -> Dict:
252255
logger.error(f"[CoachAgent] socratic LLM 실패: {e}")
253256
message = COMPONENT_HINTS.get(priority, f"'{priority}'에 대해 생각해보세요.")
254257

258+
# 우선순위 컴포넌트 재계산 (highlight용)
259+
priority = missing[0] if missing else None
260+
for req in state["mission_required"]:
261+
if req in missing:
262+
priority = req
263+
break
264+
255265
return {
256266
"message": message,
257267
"missing_components": missing,
268+
"highlight_component": priority, # ✅ [Agent 행동]
258269
"type": "missing_component",
259270
"level": 2,
260271
}
@@ -291,9 +302,16 @@ def _generate_escalate_hint(state: CoachAgentState, missing: list) -> Dict:
291302
logger.error(f"[CoachAgent] escalate LLM 실패: {e}")
292303
message = f"🚨 {missing_str} 컴포넌트를 지금 바로 캔버스에 추가하세요. 왼쪽 패널에서 드래그해서 배치할 수 있습니다."
293304

305+
priority = missing[0] if missing else None
306+
for req in state["mission_required"]:
307+
if req in missing:
308+
priority = req
309+
break
310+
294311
return {
295312
"message": message,
296313
"missing_components": missing,
314+
"highlight_component": priority, # ✅ [Agent 행동]
297315
"type": "escalate",
298316
"level": 3,
299317
}

backend/core/services/wars/agents/orchestrator/nodes.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,34 @@ def observe_game_state(state: OrchestratorState) -> OrchestratorState:
122122
player_line += f", ⚠️감지된아키이슈={arch_issues}"
123123
lines.append(player_line)
124124

125+
# ✅ [Agent 행동] 두 플레이어 비교 분석 — Orchestrator가 게임 밸런싱 판단
126+
leading_sid = None
127+
trailing_sid = None
128+
node_gap = 0
129+
130+
if len(snapshots) == 2:
131+
sids = list(snapshots.keys())
132+
n0 = snapshots[sids[0]]['node_count']
133+
n1 = snapshots[sids[1]]['node_count']
134+
node_gap = abs(n0 - n1)
135+
136+
if node_gap >= 2: # 2개 이상 차이 나면 격차 감지
137+
leading_sid = sids[0] if n0 > n1 else sids[1]
138+
trailing_sid = sids[1] if n0 > n1 else sids[0]
139+
lines.append(
140+
f"⚠️ 설계 격차 감지: {node_gap}개 차이 — "
141+
f"선두={leading_sid[:8]}, 지연={trailing_sid[:8]}"
142+
)
143+
125144
summary = "\n".join(lines)
126145
logger.info(f"[Orchestrator] 상황 요약:\n{summary}")
127-
return {**state, "situation_summary": summary}
146+
return {
147+
**state,
148+
"situation_summary": summary,
149+
"leading_sid": leading_sid,
150+
"trailing_sid": trailing_sid,
151+
"node_gap": node_gap,
152+
}
128153

129154

130155
# ─────────────────────────────────────────────────────────────────────────────
@@ -200,6 +225,19 @@ def _llm_decide(state: OrchestratorState) -> List[Dict[str, Any]]:
200225
"arch_issues_detected": arch_issues,
201226
})
202227

228+
# ✅ 격차 정보 추출
229+
leading_sid = state.get('leading_sid')
230+
trailing_sid = state.get('trailing_sid')
231+
node_gap = state.get('node_gap', 0)
232+
gap_context = ""
233+
if leading_sid and trailing_sid:
234+
gap_context = f"""
235+
[실시간 격차 감지]
236+
{leading_sid[:8]}{trailing_sid[:8]}보다 {node_gap}개 앞서있습니다.
237+
→ 뒤캘지는 플레이어({trailing_sid[:8]})에게 Coach 우선 개입을 고려하세요.
238+
→ 앞선 플레이어({leading_sid[:8]})에게는 Chaos 발동으로 역전 기회를 부여하는 것을 고려하세요.
239+
"""
240+
203241
response = _get_client().chat.completions.create(
204242
model="gpt-4o-mini",
205243
messages=[
@@ -237,7 +275,7 @@ def _llm_decide(state: OrchestratorState) -> List[Dict[str, Any]]:
237275
238276
[플레이어 상세 데이터]
239277
{json.dumps(player_info, ensure_ascii=False, indent=2)}
240-
278+
{gap_context}
241279
[핵심 제약]
242280
- chaos_triggered: {state.get('chaos_triggered')} → True면 chaos 절대 불가
243281
- 전체 배치 노드 합계: {total_nodes}개 → {_CHAOS_MIN_NODES}개 미만이면 chaos 불가
@@ -338,15 +376,22 @@ def _rule_based_action_plan(state: OrchestratorState) -> List[Dict[str, Any]]:
338376
plan = []
339377

340378
if can_trigger_coach(tmp_room, trigger_sid):
379+
# ✅ [격차 반영] 뒤쳌지는 플레이어에게 Coach 우선
380+
target_coach_sid = state.get('trailing_sid') or trigger_sid
341381
plan.append({
342-
"agent": "coach", "sid": trigger_sid,
343-
"reason": "룰 기반: 헤매는 상황 감지"
382+
"agent": "coach", "sid": target_coach_sid,
383+
"reason": "룰 기반: 헤매는 상황 감지" + (
384+
f" (압도적 격차로 인한 Coach 우선)" if state.get('node_gap', 0) >= 2 else ""
385+
)
344386
})
345387

346388
if can_trigger_chaos(tmp_room):
347389
plan.append({
348390
"agent": "chaos", "sid": None,
349-
"reason": "룰 기반: 설계 시작됨 + chaos 발동 조건 충족"
391+
"reason": "룰 기반: 설계 시작됨 + chaos 발동 조건 충족" + (
392+
f" (앞선 플레이어 {state.get('leading_sid', '')[:8]}에게 펕디쿥제)"
393+
if state.get('node_gap', 0) >= 2 else ""
394+
)
350395
})
351396

352397
if not plan:

backend/core/socket_server.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ async def draw_start(sid, data):
227227
p['last_nodes'] = []
228228
p['last_arrows'] = []
229229
p['submitted'] = False
230+
# ✅ [Agent 행동] Chaos 채점 룰 초기화 (신규 게임 시작)
231+
room['chaos_bonus_check'] = None
230232
# [수정일: 2026-03-03] DB(unit03) 미션 lazy 로드
231233
missions = await _get_missions()
232234
if not missions:
@@ -282,8 +284,17 @@ async def run_draw_room_tick(room_id):
282284
print(f"⏰ [Poll] Hint pushed to {actual_target[:8]}")
283285
await sio.emit('coach_hint', hint_payload, to=actual_target)
284286
if res.get('chaos_event'):
287+
chaos_ev = res['chaos_event']
285288
print(f"⏰ [Poll] Chaos pushed in Room: {room_id}")
286-
await sio.emit('chaos_event', res['chaos_event'], room=room_id)
289+
# ✅ [Agent 행동] Poll에서 오는 Chaos도 동일하게 게임 룰 수정
290+
req_comp = chaos_ev.get('required_component')
291+
if req_comp and room_id in draw_rooms:
292+
draw_rooms[room_id]['chaos_bonus_check'] = {
293+
'component': req_comp,
294+
'bonus_pts': 20,
295+
'penalty_pts': -10,
296+
}
297+
await sio.emit('chaos_event', chaos_ev, room=room_id)
287298

288299
await asyncio.sleep(8) # 8초마다 체크 (trigger_policy 임계값과 상충되지 않는 주기로 선정)
289300
print(f"🛑 [ArchDraw] Periodic check stopped for room: {room_id}")
@@ -310,7 +321,18 @@ async def draw_submit(sid, data):
310321
complete_bonus = 20 if ratio == 1.0 else 0 # 전부 통과 시 +20점
311322
combo_bonus = min(data.get('combo', 0), 5) * 2 # 최대 10점 (5콤보 상한)
312323
pts = check_score + time_bonus + complete_bonus + combo_bonus
313-
324+
325+
# ✅ [Agent 행동 반영] ChaosAgent가 설정한 게임 룰 체크
326+
chaos_check = room.get('chaos_bonus_check')
327+
if chaos_check:
328+
placed_ids = [n.get('compId') for n in data.get('final_nodes', [])]
329+
if chaos_check['component'] in placed_ids:
330+
pts += chaos_check['bonus_pts']
331+
print(f"✅ [ArchDraw] Chaos 조건 충족: {player['name']} +{chaos_check['bonus_pts']}점 ({chaos_check['component']} 배치 확인)")
332+
else:
333+
pts += chaos_check['penalty_pts'] # 음수값
334+
print(f"❌ [ArchDraw] Chaos 조건 미충족: {player['name']} {chaos_check['penalty_pts']}점 ({chaos_check['component']} 누락)")
335+
314336
player['score'] += pts
315337
player['last_pts'] = pts
316338
player['last_checks'] = checks
@@ -439,6 +461,8 @@ async def draw_next_round(sid, data):
439461
room_state.hint_history = {}
440462
room_state.past_event_ids = []
441463
room_state.player_designs = {}
464+
# ✅ [Agent 행동] 다음 라운드에서 Chaos 채점 룰 리셋
465+
room['chaos_bonus_check'] = None
442466

443467
# 새로운 무작위 문제 선택 + 팔레트 서버 생성
444468
cur_round = data.get('round', room.get('current_round', 1) + 1)
@@ -487,9 +511,19 @@ async def draw_canvas_sync(sid, data):
487511
target_sid = res['coach_hint'].get('_target_sid', sid)
488512
print(f"💡 [ArchDraw] Hint Sent to {target_sid[:8]}: {hint_payload.get('message', '')[:20]}...")
489513
await sio.emit('coach_hint', hint_payload, to=target_sid) # [수정일: 2026-03-03] room= → to= (개인 전송)
490-
if res.get('chaos_event'):
491-
print(f"🔥 [ArchDraw] Chaos Event in Room: {room_id}")
492-
await sio.emit('chaos_event', res['chaos_event'], room=room_id)
514+
if res.get('chaos_event'):
515+
chaos_ev = res['chaos_event']
516+
print(f"🔥 [ArchDraw] Chaos Event in Room: {room_id} | required_component={chaos_ev.get('required_component')}")
517+
# ✅ [Agent 행동] Chaos가 게임 룰을 직접 수정 — 채점 시 반영됨
518+
req_comp = chaos_ev.get('required_component')
519+
if req_comp:
520+
draw_rooms[room_id]['chaos_bonus_check'] = {
521+
'component': req_comp,
522+
'bonus_pts': 20,
523+
'penalty_pts': -10,
524+
}
525+
print(f"📋 [ArchDraw] Chaos 채점 룰 추가: +20 if {req_comp} placed, -10 if not")
526+
await sio.emit('chaos_event', chaos_ev, room=room_id)
493527

494528
@sio.event
495529
async def draw_chaos_complete(sid, data):

frontend/src/features/wars/WarsModeSelect.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ const goTo = (path) => router.push(path);
259259
}
260260
261261
.exit-btn {
262-
position: fixed; top: 1.5rem; left: 1.5rem;
262+
position: fixed; top: 1.5rem; right: 1.5rem;
263263
background: rgba(255,255,255,0.04);
264264
border: 1px solid rgba(255,255,255,0.1);
265265
color: #94a3b8;

frontend/src/features/wars/composables/useDrawSocket.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function useDrawSocket() {
2727
const onChaosRecovered = ref(null) // [추가 2026-03-03] 장애 복구 알림
2828
const onGameOver = ref(null)
2929

30-
function connect(roomId, userName, userId) {
30+
function connect(roomId, userName, userId, avatarUrl) {
3131
if (socket.value) return
3232

3333
// [수정일: 2026-03-01] 소켓 URL 결정 로직 수정
@@ -50,7 +50,7 @@ export function useDrawSocket() {
5050
socket.value.on('connect', () => {
5151
connected.value = true
5252
// [수정일: 2026-03-03] DB 연동을 위해 user_id 포함하여 전송
53-
socket.value.emit('draw_join', { room_id: roomId, user_name: userName, user_id: userId })
53+
socket.value.emit('draw_join', { room_id: roomId, user_name: userName, user_id: userId, avatar_url: avatarUrl || '' })
5454
})
5555

5656
socket.value.on('disconnect', () => { connected.value = false })

0 commit comments

Comments
 (0)