feat(engine): 实现阵营投票系统
- 新增 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 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from .social_influence import apply_social_influence
|
|||||||
from .factions import (
|
from .factions import (
|
||||||
update_factions, apply_faction_influence, check_and_trigger_faction_event
|
update_factions, apply_faction_influence, check_and_trigger_faction_event
|
||||||
)
|
)
|
||||||
|
from .voting import process_votes, apply_votes_to_factions
|
||||||
|
|
||||||
MAX_EVENTS = 20
|
MAX_EVENTS = 20
|
||||||
MAX_MEMORY_PER_AGENT = 3
|
MAX_MEMORY_PER_AGENT = 3
|
||||||
@@ -43,6 +44,9 @@ def process_events(state: WorldState, events: List[Event]) -> WorldState:
|
|||||||
# tick 递增
|
# tick 递增
|
||||||
state.tick += 1
|
state.tick += 1
|
||||||
|
|
||||||
|
# ★ 在 tick 开始时:将上一轮投票累加到阵营能量
|
||||||
|
apply_votes_to_factions(state)
|
||||||
|
|
||||||
# cooldown 递减
|
# cooldown 递减
|
||||||
if state.global_meter.cooldown > 0:
|
if state.global_meter.cooldown > 0:
|
||||||
state.global_meter.cooldown -= 1
|
state.global_meter.cooldown -= 1
|
||||||
@@ -50,6 +54,9 @@ def process_events(state: WorldState, events: List[Event]) -> WorldState:
|
|||||||
# 更新世界效果(持续影响)
|
# 更新世界效果(持续影响)
|
||||||
update_world_effects(state)
|
update_world_effects(state)
|
||||||
|
|
||||||
|
# ★ 处理投票事件(记录到 votes,下一 tick 生效)
|
||||||
|
process_votes(state, events)
|
||||||
|
|
||||||
for event in events:
|
for event in events:
|
||||||
text = event.text
|
text = event.text
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,14 @@ class Factions(BaseModel):
|
|||||||
fearful: FactionData = Field(default_factory=FactionData)
|
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):
|
class FactionEventResult(BaseModel):
|
||||||
"""阵营事件触发结果"""
|
"""阵营事件触发结果"""
|
||||||
type: Optional[str] = None # "festival" | "panic" | None
|
type: Optional[str] = None # "festival" | "panic" | None
|
||||||
@@ -104,6 +112,7 @@ class WorldState(BaseModel):
|
|||||||
world_effects: List[WorldEffect] = Field(default_factory=list)
|
world_effects: List[WorldEffect] = Field(default_factory=list)
|
||||||
factions: Factions = Field(default_factory=Factions)
|
factions: Factions = Field(default_factory=Factions)
|
||||||
story_arcs: Dict[str, StoryArc] = Field(default_factory=dict)
|
story_arcs: Dict[str, StoryArc] = Field(default_factory=dict)
|
||||||
|
votes: Votes = Field(default_factory=Votes)
|
||||||
|
|
||||||
|
|
||||||
class Event(BaseModel):
|
class Event(BaseModel):
|
||||||
@@ -111,6 +120,7 @@ class Event(BaseModel):
|
|||||||
text: str
|
text: str
|
||||||
user: str
|
user: str
|
||||||
ts: float
|
ts: float
|
||||||
|
faction: Optional[str] = None # 用于投票事件: "optimists" | "fearful"
|
||||||
|
|
||||||
|
|
||||||
class StepRequest(BaseModel):
|
class StepRequest(BaseModel):
|
||||||
|
|||||||
46
engine-python/app/voting.py
Normal file
46
engine-python/app/voting.py
Normal file
@@ -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()
|
||||||
@@ -381,3 +381,103 @@ def test_story_event_in_response(client):
|
|||||||
assert "story_event" in data
|
assert "story_event" in data
|
||||||
assert "triggered" in data["story_event"]
|
assert "triggered" in data["story_event"]
|
||||||
assert "arc_id" 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
|
||||||
|
|||||||
Reference in New Issue
Block a user