From 75e84f2ba35f4860defdc692fd103b9de5a6805d Mon Sep 17 00:00:00 2001 From: empty Date: Tue, 30 Dec 2025 13:12:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(engine):=20=E5=AE=9E=E7=8E=B0=E9=98=B5?= =?UTF-8?q?=E8=90=A5=E6=8A=95=E7=A5=A8=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Votes 模型,包含 optimists/fearful 计数和 voted_users 去重列表 - 扩展 Event 模型,添加可选 faction 字段支持投票事件 - 新增 voting.py 模块处理投票逻辑 - 投票规则:每用户每 tick 限投一次,下一 tick 生效 - 投票累加到 factions.power,达到 threshold 触发阵营技能 - 添加 5 个投票系统测试用例,全部 26 个测试通过 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- engine-python/app/engine.py | 7 +++ engine-python/app/models.py | 10 ++++ engine-python/app/voting.py | 46 ++++++++++++++ engine-python/tests/test_step.py | 100 +++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) create mode 100644 engine-python/app/voting.py diff --git a/engine-python/app/engine.py b/engine-python/app/engine.py index 4a04c6a..5e1a283 100644 --- a/engine-python/app/engine.py +++ b/engine-python/app/engine.py @@ -10,6 +10,7 @@ from .social_influence import apply_social_influence from .factions import ( update_factions, apply_faction_influence, check_and_trigger_faction_event ) +from .voting import process_votes, apply_votes_to_factions MAX_EVENTS = 20 MAX_MEMORY_PER_AGENT = 3 @@ -43,6 +44,9 @@ def process_events(state: WorldState, events: List[Event]) -> WorldState: # tick 递增 state.tick += 1 + # ★ 在 tick 开始时:将上一轮投票累加到阵营能量 + apply_votes_to_factions(state) + # cooldown 递减 if state.global_meter.cooldown > 0: state.global_meter.cooldown -= 1 @@ -50,6 +54,9 @@ def process_events(state: WorldState, events: List[Event]) -> WorldState: # 更新世界效果(持续影响) update_world_effects(state) + # ★ 处理投票事件(记录到 votes,下一 tick 生效) + process_votes(state, events) + for event in events: text = event.text diff --git a/engine-python/app/models.py b/engine-python/app/models.py index 9db8d3b..4514ceb 100644 --- a/engine-python/app/models.py +++ b/engine-python/app/models.py @@ -58,6 +58,14 @@ class Factions(BaseModel): fearful: FactionData = Field(default_factory=FactionData) +class Votes(BaseModel): + """投票统计""" + optimists: int = 0 + fearful: int = 0 + # 记录本 tick 已投票的用户(用于去重) + voted_users: List[str] = Field(default_factory=list) + + class FactionEventResult(BaseModel): """阵营事件触发结果""" type: Optional[str] = None # "festival" | "panic" | None @@ -104,6 +112,7 @@ class WorldState(BaseModel): world_effects: List[WorldEffect] = Field(default_factory=list) factions: Factions = Field(default_factory=Factions) story_arcs: Dict[str, StoryArc] = Field(default_factory=dict) + votes: Votes = Field(default_factory=Votes) class Event(BaseModel): @@ -111,6 +120,7 @@ class Event(BaseModel): text: str user: str ts: float + faction: Optional[str] = None # 用于投票事件: "optimists" | "fearful" class StepRequest(BaseModel): diff --git a/engine-python/app/voting.py b/engine-python/app/voting.py new file mode 100644 index 0000000..c2065ee --- /dev/null +++ b/engine-python/app/voting.py @@ -0,0 +1,46 @@ +"""阵营投票系统 - 允许用户通过投票影响阵营能量""" +from typing import List +from .models import WorldState, Event, Votes + + +def process_votes(state: WorldState, events: List[Event]) -> None: + """处理投票事件 + + 规则: + 1. 每个用户在一个 tick 内只能投 1 次票(按 user 去重) + 2. 每票增加对应 faction 的 votes += 1 + """ + for event in events: + if event.type != "vote": + continue + + # 验证 faction 有效性 + if event.faction not in ("optimists", "fearful"): + continue + + # 检查用户是否已投票(去重) + if event.user in state.votes.voted_users: + continue + + # 记录投票 + state.votes.voted_users.append(event.user) + + if event.faction == "optimists": + state.votes.optimists += 1 + elif event.faction == "fearful": + state.votes.fearful += 1 + + +def apply_votes_to_factions(state: WorldState) -> None: + """将投票累加到阵营能量,然后清空投票 + + 在每个 tick 开始时调用: + 1. 将 votes 累加进 factions.power + 2. 清空 votes(包括 voted_users) + """ + # 累加投票到阵营能量 + state.factions.optimists.power += state.votes.optimists + state.factions.fearful.power += state.votes.fearful + + # 清空投票 + state.votes = Votes() diff --git a/engine-python/tests/test_step.py b/engine-python/tests/test_step.py index 7a10727..e7db159 100644 --- a/engine-python/tests/test_step.py +++ b/engine-python/tests/test_step.py @@ -381,3 +381,103 @@ def test_story_event_in_response(client): assert "story_event" in data assert "triggered" in data["story_event"] assert "arc_id" in data["story_event"] + + +# ==================== 投票系统测试 ==================== + +def test_votes_exists_in_world_state(client): + """测试 world_state 包含 votes 字段""" + resp = client.post("/step", json={"events": []}) + data = resp.json() + + assert "votes" in data["world_state"] + votes = data["world_state"]["votes"] + assert "optimists" in votes + assert "fearful" in votes + assert "voted_users" in votes + + +def test_vote_records_correctly(client): + """测试投票正确记录""" + events = [ + {"type": "vote", "faction": "optimists", "text": "", "user": "user1", "ts": 1}, + {"type": "vote", "faction": "fearful", "text": "", "user": "user2", "ts": 2}, + ] + resp = client.post("/step", json={"events": events}) + data = resp.json() + + votes = data["world_state"]["votes"] + assert votes["optimists"] == 1 + assert votes["fearful"] == 1 + assert "user1" in votes["voted_users"] + assert "user2" in votes["voted_users"] + + +def test_vote_user_deduplication(client): + """测试同一用户在同一 tick 只能投一次票""" + events = [ + {"type": "vote", "faction": "optimists", "text": "", "user": "user1", "ts": 1}, + {"type": "vote", "faction": "optimists", "text": "", "user": "user1", "ts": 2}, + {"type": "vote", "faction": "fearful", "text": "", "user": "user1", "ts": 3}, + ] + resp = client.post("/step", json={"events": events}) + data = resp.json() + + votes = data["world_state"]["votes"] + # 只计算第一次投票 + assert votes["optimists"] == 1 + assert votes["fearful"] == 0 + + +def test_votes_apply_to_faction_power_next_tick(client): + """测试投票在下一个 tick 累加到阵营能量""" + # 第一个 tick:投票 + events = [ + {"type": "vote", "faction": "optimists", "text": "", "user": "user1", "ts": 1}, + {"type": "vote", "faction": "optimists", "text": "", "user": "user2", "ts": 2}, + {"type": "vote", "faction": "fearful", "text": "", "user": "user3", "ts": 3}, + ] + resp1 = client.post("/step", json={"events": events}) + data1 = resp1.json() + + # 记录第一个 tick 后的阵营能量 + power1_opt = data1["world_state"]["factions"]["optimists"]["power"] + power1_fear = data1["world_state"]["factions"]["fearful"]["power"] + + # 第二个 tick:投票应该生效 + resp2 = client.post("/step", json={"events": []}) + data2 = resp2.json() + + power2_opt = data2["world_state"]["factions"]["optimists"]["power"] + power2_fear = data2["world_state"]["factions"]["fearful"]["power"] + + # 验证投票已累加(+2 optimists, +1 fearful) + assert power2_opt >= power1_opt + 2 + assert power2_fear >= power1_fear + 1 + + # 验证投票已清空 + assert data2["world_state"]["votes"]["optimists"] == 0 + assert data2["world_state"]["votes"]["fearful"] == 0 + + +def test_votes_trigger_faction_event(client): + """测试投票累积触发阵营技能""" + # 多轮投票累积能量直到触发 + for i in range(5): + events = [ + {"type": "vote", "faction": "optimists", "text": "", "user": f"user_{i}_a", "ts": i}, + {"type": "vote", "faction": "optimists", "text": "", "user": f"user_{i}_b", "ts": i + 0.1}, + ] + resp = client.post("/step", json={"events": events}) + + # 再投一轮让投票生效 + resp = client.post("/step", json={"events": []}) + data = resp.json() + + # 验证阵营能量已累积(可能已触发并重置) + # 如果触发了,power 会重置为 0 + factions = data["world_state"]["factions"] + triggered = data["triggered_faction_event"]["type"] is not None + + # 要么触发了事件,要么能量在累积 + assert triggered or factions["optimists"]["power"] > 0