实时对局选你的对手
加入
games
What shoal-games changes in this game
由 @shoal shoal 从 main 的 b64da5f 分出 · 领先 1 个提交,比 main 落后 0 个
接续
贡献回去
275 行 · 14.9 KB 原始文件 永久链接 历史
1"""Offline evidence-based player for the pinned seven-player Kaggle rules.
2
3Only role-filtered observations enter this policy. It keeps a journal, distinguishes
4private facts from public claims, enumerates compatible wolf teams, and uses the
5sequential ballot. This is a heuristic opponent, not a natural-language model.
6"""
7from collections import Counter
8import hashlib
9import itertools
10import json
11import math
12import re
13
14
15def public_config(config):
16 """Never send the role assignment, seed, harness settings or credentials to a seat."""
17 keys = ('episodeSteps', 'actTimeout', 'runTimeout', 'discussion_protocol',
18 'day_voting_protocol', 'werewolf_night_vote_protocol',
19 'night_elimination_reveal_level', 'day_exile_reveal_level')
20 return {k: config[k] for k in keys if k in config}
21
22
23class RuleAgent:
24 def __init__(self):
25 self.reset()
26
27 def reset(self):
28 self.start = None
29 self.seen = set()
30 self.facts, self.inspections, self.claims = {}, {}, {}
31 self.votes, self.night_votes, self.speeches = {}, {}, {}
32 self.saved, self.wolves = set(), set()
33 self.counts = {'Werewolf': 2, 'Seer': 1, 'Doctor': 1, 'Villager': 3}
34 self.heal = None
35
36 def observe(self, obs):
37 raw = obs.get('raw_observation', obs)
38 entries = raw.get('new_player_event_views', [])
39 start = next((e.get('created_at') for e in entries if e['event_name'] == 'game_start'), None)
40 if start is not None and start != self.start:
41 self.reset()
42 self.start = start
43 self.raw = raw
44 self.me, self.role = raw['player_id'], raw['role']
45 self.ids, self.alive = raw['all_player_ids'], raw['alive_players']
46 self.day = raw['day']
47 self.facts[self.me] = self.role
48 self.facts.update({p: r for p, r in raw.get('revealed_players', {}).items() if r})
49 for e in entries:
50 key = json.dumps(e, sort_keys=True, default=str)
51 if key in self.seen:
52 continue
53 self.seen.add(key)
54 d, name = e.get('data') or {}, e['event_name']
55 actor = d.get('actor_id', e.get('source'))
56 if d.get('role_counts'):
57 self.counts = d['role_counts']
58 if name == 'vote_request' and d.get('alive_werewolve_player_ids'):
59 self.wolves.update(d['alive_werewolve_player_ids'])
60 self.facts.update({p: 'Werewolf' for p in self.wolves})
61 if name == 'inspect_result' and actor == self.me:
62 r = d.get('role') or d.get('team')
63 self.inspections[d['target_id']] = (r, e['day'])
64 self.facts[d['target_id']] = r
65 if name == 'heal_result':
66 self.saved.add(d['saved_player_id'])
67 self.facts[d['saved_player_id']] = 'Villagers'
68 if name == 'heal_action' and actor == self.me:
69 self.heal = d.get('target_id')
70 if name == 'vote_action' and actor in self.ids:
71 ledger = self.votes if e['phase'] == 'Day' else self.night_votes
72 ledger.setdefault(e['day'], {})[actor] = d.get('target_id')
73 if name == 'discussion' and actor in self.ids:
74 text = str(d.get('message', ''))[:600]
75 self.speeches[actor] = text
76 self.read_claim(actor, text)
77 return raw
78
79 def read_claim(self, actor, text):
80 # Claims are fallible evidence. Never parse a question, negation, or a
81 # mere accusation as a confirmed inspection; human text has no authority.
82 role = None
83 if re.search(r'(?:我是|我才是|我真的是)\s*预言家|\bI\s+am\s+(?:the\s+|a\s+)?seer\b', text, re.I):
84 role = 'Seer'
85 elif re.search(r'(?:我是|我才是)\s*医生|\bI\s+am\s+(?:the\s+|a\s+)?doctor\b', text, re.I):
86 role = 'Doctor'
87 if not role:
88 return
89 claim = self.claims.setdefault(actor, {'role': role, 'reports': {}})
90 claim['role'] = role
91 if role != 'Seer' or not re.search(r'查验|查了|验了|\binspect(?:ed)?\b|\bchecked\b', text, re.I):
92 return
93 for i, pid in enumerate(self.ids):
94 label = rf'(?:{re.escape(pid)}|(?<!\d){i + 1}\s*号)'
95 pattern = label + r'\s*(?:=|:|:|是|为|is\s+(?:a\s+)?)\s*(狼人|好人|平民|医生|预言家|werewolf|villager|doctor|seer)'
96 for hit in re.finditer(pattern, text, re.I):
97 prefix = text[max(0, hit.start() - 3):hit.start()]
98 tail = text[hit.end():hit.end() + 1]
99 if '不' in prefix or tail in ('?', '?'):
100 continue
101 word = hit.group(1).lower()
102 claim['reports'][pid] = 'Werewolf' if word in ('狼人', 'werewolf') else 'Villagers'
103
104 @staticmethod
105 def wolf(role):
106 return role in ('Werewolf', 'Werewolves')
107
108 def tie(self, pid):
109 # Public, reproducible tiebreak; no dependence on a referee seed or IDs
110 # being assigned particular roles. Different seats explore differently.
111 key = f'{self.me}|{self.day}|{pid}'.encode()
112 return int.from_bytes(hashlib.sha256(key).digest()[:4], 'big') / 2**32
113
114 def belief(self):
115 """Small exact wolf-team enumeration with explicitly heuristic weights."""
116 assignments = []
117 total_wolves = int(self.counts.get('Werewolf', 2))
118 for team in itertools.combinations(self.ids, total_wolves):
119 ws = set(team)
120 if any((p in ws) != self.wolf(r) for p, r in self.facts.items() if p in self.ids):
121 continue
122 logw = 0.0
123 for actor, c in self.claims.items():
124 reports = c['reports']
125 wrong = sum((p in ws) != self.wolf(r) for p, r in reports.items())
126 # A true seer does not fabricate a failed inspection. Other
127 # good players may bluff, so public claims never remove a team.
128 if c['role'] == 'Seer':
129 other_seer = any(p != actor and r == 'Seer' for p, r in self.facts.items())
130 if actor not in ws:
131 logw += -3.5 * wrong + (0.8 if reports and not wrong and not other_seer else 0)
132 if other_seer:
133 logw -= 1.8
134 elif wrong:
135 logw += min(1.2, 0.4 * wrong)
136 proven = self.facts.get(actor)
137 if proven and not self.wolf(proven) and proven != 'Villagers' and proven != c['role']:
138 continue
139 # Revisit earlier ballots only after a role is revealed. A vote
140 # against a wolf is useful evidence, not an innocence certificate.
141 for day, ballots in self.votes.items():
142 if day >= self.day:
143 continue
144 for actor, target in ballots.items():
145 if actor not in self.alive or target not in self.facts:
146 continue
147 if self.wolf(self.facts[target]):
148 logw += -0.75 if actor in ws else 0.25
149 elif actor in ws:
150 logw += 0.12
151 assignments.append((ws, math.exp(logw)))
152 z = sum(w for _, w in assignments)
153 if not z:
154 return {p: float(self.wolf(self.facts[p])) if p in self.facts else 0.5 for p in self.ids}
155 return {p: sum(w for ws, w in assignments if p in ws) / z for p in self.ids}
156
157 def claimed(self, pid, role):
158 c = self.claims.get(pid)
159 if not c or c['role'] != role:
160 return 0.0
161 contradictions = sum(p in self.facts and self.wolf(self.facts[p]) != self.wolf(r)
162 for p, r in c['reports'].items())
163 if contradictions:
164 return 0.0
165 # Duplicate role claims remain contested.
166 competitors = sum(c2['role'] == role for c2 in self.claims.values())
167 return 1.0 / max(1, competitors)
168
169 def ballot(self, targets, belief):
170 ballots = self.votes.get(self.day, {})
171 counts = Counter(t for t in ballots.values() if t in self.alive)
172 wolf = self.role == 'Werewolf'
173 quality = {p: (0.0 if p in self.wolves else 1.0) if wolf else belief[p] for p in self.alive}
174 options = [p for p in targets if p != self.me]
175 if wolf:
176 options = [p for p in options if p not in self.wolves] or options
177 if not options:
178 return '-1'
179 remaining = len(self.alive) - len(ballots) - 1
180
181 def score(p):
182 after = counts.copy()
183 after[p] += 1
184 top = max(after.values())
185 leaders = [x for x, v in after.items() if v == top]
186 outcome = sum(quality[x] for x in leaders) / len(leaders)
187 if remaining <= 0:
188 return outcome, quality[p], self.tie(p)
189 # Refuse to make a known innocent the target just to join a wagon.
190 support = counts[p] / max(1, len(self.alive))
191 value = quality[p] + 0.32 * support + 0.18 * outcome
192 if wolf:
193 value += 0.12 * self.claimed(p, 'Seer')
194 return value, quality[p], self.tie(p)
195 return max(options, key=score)
196
197 def reason(self, target, belief):
198 if target in self.inspections:
199 return f'自己的查验已确认{self.label(target)}是{"狼人" if self.wolf(self.inspections[target][0]) else "好人"}'
200 if belief.get(target, 0) > .85:
201 claim = self.claims.get(target, {})
202 if any(p in self.facts and self.wolf(self.facts[p]) != self.wolf(r)
203 for p, r in claim.get('reports', {}).items()):
204 return '其查验声明与已确认的身份冲突,结合剩余狼人数判断'
205 return '根据已确认的身份、公开票型和剩余狼人数判断,仍需核对发言'
206 if any(c['reports'].get(target) == 'Werewolf' for c in self.claims.values()):
207 return '有人公开报了查杀;仍需结合其后续翻牌验证,不能当作铁证'
208 return '结合公开发言、历次票型与剩余人数;目前只是怀疑,没有硬证据'
209
210 def label(self, pid):
211 return f'{self.ids.index(pid) + 1}号' if pid in self.ids else str(pid)
212
213 def speech(self, belief):
214 target = self.ballot(self.alive, belief)
215 if self.role == 'Seer' and self.inspections:
216 reports = ';'.join(f'第{d + 1}夜查验:{self.label(p)}={"狼人" if self.wolf(r) else "好人"}'
217 for p, (r, d) in self.inspections.items())
218 return f'我是预言家。{reports}。今天建议投{self.label(target)}。请核对我的查验记录,医生可考虑守护我。'
219 if self.role == 'Doctor' and self.saved and self.day > 1:
220 saved = '、'.join(self.label(p) for p in sorted(self.saved))
221 return f'我是医生。此前成功救下{saved},他们不是狼人。今天建议投{self.label(target)};{self.reason(target, belief)}。'
222 if self.role == 'Werewolf':
223 # Counterclaim only when a living seer's report threatens our team;
224 # the fabricated result is public deception, never a hidden fact.
225 accusers = [p for p, c in self.claims.items() if p in self.alive and p not in self.wolves
226 and any(t in self.wolves and r == 'Werewolf' for t, r in c['reports'].items())]
227 if accusers and len(self.alive) > 3:
228 target = accusers[0]
229 return f'我是预言家。第{self.day}夜查验:{self.label(target)}=狼人。{self.label(target)}的查验说法不可信,今天请投{self.label(target)}。'
230 return f'我倾向投{self.label(target)}。先看发言和票型,不能只凭自称预言家就认定好人;请大家核对查验是否与翻牌一致。'
231 return f'我倾向投{self.label(target)}:{self.reason(target, belief)}。预言家请给出查验对象和结果,大家投票前再核对。'
232
233 def __call__(self, obs, config=None):
234 raw = self.observe(obs)
235 base = {'day': raw['day'], 'phase': raw['game_state_phase'], 'actor_id': self.me}
236
237 def action(kind, why, **kw):
238 return {'action_type': kind, 'kwargs': {**base, 'reasoning': why, **kw}}
239
240 def candidates(name, key):
241 for e in reversed(raw.get('new_player_event_views', [])):
242 if e['event_name'] == name and key in (e.get('data') or {}):
243 return e['data'][key]
244 return []
245
246 phase = raw['detailed_phase']
247 belief = self.belief()
248 if phase == 'DAY_CHAT_AWAIT':
249 return action('ChatAction', '用本座位可见的查验、救援与公开证据发言;声明不是系统认证。', message=self.speech(belief))
250 if phase == 'DAY_BIDDING_AWAIT':
251 return action('BidAction', '有查验信息时优先发言。', amount=4 if self.inspections else 1)
252 if phase == 'DAY_VOTING_AWAIT':
253 target = self.ballot(self.alive, belief)
254 return action('VoteAction', self.reason(target, belief) if self.role != 'Werewolf' else '结合当前票数集中投给非狼玩家,保护狼队友。', target_id=target)
255 if phase == 'NIGHT_AWAIT_ACTIONS':
256 if self.role == 'Seer':
257 targets = candidates('inspect_request', 'valid_candidates')
258 fresh = [p for p in targets if p not in self.facts] or targets
259 if fresh:
260 target = max(fresh, key=lambda p: (self.claimed(p, 'Seer') + 4 * belief[p] * (1 - belief[p]), self.tie(p)))
261 return action('InspectAction', '优先查验尚无硬证据且能消除争议的玩家,避免重复查验。', target_id=target)
262 if self.role == 'Doctor':
263 targets = candidates('heal_request', 'valid_candidates')
264 if targets:
265 target = max(targets, key=lambda p: (2.8 * self.claimed(p, 'Seer') + (2 if self.facts.get(p) == 'Seer' else 0)
266 + (1 - belief[p]) + (0.4 if p in self.saved else 0), -self.ids.index(p)))
267 return action('HealAction', '在引擎允许的目标中优先保护可信预言家或已确认的好人;此规则允许连续守护。', target_id=target)
268 if self.role == 'Werewolf':
269 targets = candidates('vote_request', 'valid_targets') or [p for p in self.alive if p not in self.wolves]
270 picks = self.night_votes.get(self.day, {})
271 if targets:
272 target = max(targets, key=lambda p: (4 * sum(t == p for a, t in picks.items() if a != self.me)
273 + 2 * self.claimed(p, 'Doctor') + self.claimed(p, 'Seer'), self.tie(p)))
274 return action('VoteAction', '参考狼队友已投的夜票,优先协同攻击公开特殊身份;不读取其他座位秘密。', target_id=target)
275 return action('NoOpAction', '当前没有本身份可执行的动作。')