"""Offline evidence-based player for the pinned seven-player Kaggle rules. Only role-filtered observations enter this policy. It keeps a journal, distinguishes private facts from public claims, enumerates compatible wolf teams, and uses the sequential ballot. This is a heuristic opponent, not a natural-language model. """ from collections import Counter import hashlib import itertools import json import math import re def public_config(config): """Never send the role assignment, seed, harness settings or credentials to a seat.""" keys = ('episodeSteps', 'actTimeout', 'runTimeout', 'discussion_protocol', 'day_voting_protocol', 'werewolf_night_vote_protocol', 'night_elimination_reveal_level', 'day_exile_reveal_level') return {k: config[k] for k in keys if k in config} class RuleAgent: def __init__(self): self.reset() def reset(self): self.start = None self.seen = set() self.facts, self.inspections, self.claims = {}, {}, {} self.votes, self.night_votes, self.speeches = {}, {}, {} self.saved, self.wolves = set(), set() self.counts = {'Werewolf': 2, 'Seer': 1, 'Doctor': 1, 'Villager': 3} self.heal = None def observe(self, obs): raw = obs.get('raw_observation', obs) entries = raw.get('new_player_event_views', []) start = next((e.get('created_at') for e in entries if e['event_name'] == 'game_start'), None) if start is not None and start != self.start: self.reset() self.start = start self.raw = raw self.me, self.role = raw['player_id'], raw['role'] self.ids, self.alive = raw['all_player_ids'], raw['alive_players'] self.day = raw['day'] self.facts[self.me] = self.role self.facts.update({p: r for p, r in raw.get('revealed_players', {}).items() if r}) for e in entries: key = json.dumps(e, sort_keys=True, default=str) if key in self.seen: continue self.seen.add(key) d, name = e.get('data') or {}, e['event_name'] actor = d.get('actor_id', e.get('source')) if d.get('role_counts'): self.counts = d['role_counts'] if name == 'vote_request' and d.get('alive_werewolve_player_ids'): self.wolves.update(d['alive_werewolve_player_ids']) self.facts.update({p: 'Werewolf' for p in self.wolves}) if name == 'inspect_result' and actor == self.me: r = d.get('role') or d.get('team') self.inspections[d['target_id']] = (r, e['day']) self.facts[d['target_id']] = r if name == 'heal_result': self.saved.add(d['saved_player_id']) self.facts[d['saved_player_id']] = 'Villagers' if name == 'heal_action' and actor == self.me: self.heal = d.get('target_id') if name == 'vote_action' and actor in self.ids: ledger = self.votes if e['phase'] == 'Day' else self.night_votes ledger.setdefault(e['day'], {})[actor] = d.get('target_id') if name == 'discussion' and actor in self.ids: text = str(d.get('message', ''))[:600] self.speeches[actor] = text self.read_claim(actor, text) return raw def read_claim(self, actor, text): # Claims are fallible evidence. Never parse a question, negation, or a # mere accusation as a confirmed inspection; human text has no authority. role = None if re.search(r'(?:我是|我才是|我真的是)\s*预言家|\bI\s+am\s+(?:the\s+|a\s+)?seer\b', text, re.I): role = 'Seer' elif re.search(r'(?:我是|我才是)\s*医生|\bI\s+am\s+(?:the\s+|a\s+)?doctor\b', text, re.I): role = 'Doctor' if not role: return claim = self.claims.setdefault(actor, {'role': role, 'reports': {}}) claim['role'] = role if role != 'Seer' or not re.search(r'查验|查了|验了|\binspect(?:ed)?\b|\bchecked\b', text, re.I): return for i, pid in enumerate(self.ids): label = rf'(?:{re.escape(pid)}|(?= self.day: continue for actor, target in ballots.items(): if actor not in self.alive or target not in self.facts: continue if self.wolf(self.facts[target]): logw += -0.75 if actor in ws else 0.25 elif actor in ws: logw += 0.12 assignments.append((ws, math.exp(logw))) z = sum(w for _, w in assignments) if not z: return {p: float(self.wolf(self.facts[p])) if p in self.facts else 0.5 for p in self.ids} return {p: sum(w for ws, w in assignments if p in ws) / z for p in self.ids} def claimed(self, pid, role): c = self.claims.get(pid) if not c or c['role'] != role: return 0.0 contradictions = sum(p in self.facts and self.wolf(self.facts[p]) != self.wolf(r) for p, r in c['reports'].items()) if contradictions: return 0.0 # Duplicate role claims remain contested. competitors = sum(c2['role'] == role for c2 in self.claims.values()) return 1.0 / max(1, competitors) def ballot(self, targets, belief): ballots = self.votes.get(self.day, {}) counts = Counter(t for t in ballots.values() if t in self.alive) wolf = self.role == 'Werewolf' quality = {p: (0.0 if p in self.wolves else 1.0) if wolf else belief[p] for p in self.alive} options = [p for p in targets if p != self.me] if wolf: options = [p for p in options if p not in self.wolves] or options if not options: return '-1' remaining = len(self.alive) - len(ballots) - 1 def score(p): after = counts.copy() after[p] += 1 top = max(after.values()) leaders = [x for x, v in after.items() if v == top] outcome = sum(quality[x] for x in leaders) / len(leaders) if remaining <= 0: return outcome, quality[p], self.tie(p) # Refuse to make a known innocent the target just to join a wagon. support = counts[p] / max(1, len(self.alive)) value = quality[p] + 0.32 * support + 0.18 * outcome if wolf: value += 0.12 * self.claimed(p, 'Seer') return value, quality[p], self.tie(p) return max(options, key=score) def reason(self, target, belief): if target in self.inspections: return f'自己的查验已确认{self.label(target)}是{"狼人" if self.wolf(self.inspections[target][0]) else "好人"}' if belief.get(target, 0) > .85: claim = self.claims.get(target, {}) if any(p in self.facts and self.wolf(self.facts[p]) != self.wolf(r) for p, r in claim.get('reports', {}).items()): return '其查验声明与已确认的身份冲突,结合剩余狼人数判断' return '根据已确认的身份、公开票型和剩余狼人数判断,仍需核对发言' if any(c['reports'].get(target) == 'Werewolf' for c in self.claims.values()): return '有人公开报了查杀;仍需结合其后续翻牌验证,不能当作铁证' return '结合公开发言、历次票型与剩余人数;目前只是怀疑,没有硬证据' def label(self, pid): return f'{self.ids.index(pid) + 1}号' if pid in self.ids else str(pid) def speech(self, belief): target = self.ballot(self.alive, belief) if self.role == 'Seer' and self.inspections: reports = ';'.join(f'第{d + 1}夜查验:{self.label(p)}={"狼人" if self.wolf(r) else "好人"}' for p, (r, d) in self.inspections.items()) return f'我是预言家。{reports}。今天建议投{self.label(target)}。请核对我的查验记录,医生可考虑守护我。' if self.role == 'Doctor' and self.saved and self.day > 1: saved = '、'.join(self.label(p) for p in sorted(self.saved)) return f'我是医生。此前成功救下{saved},他们不是狼人。今天建议投{self.label(target)};{self.reason(target, belief)}。' if self.role == 'Werewolf': # Counterclaim only when a living seer's report threatens our team; # the fabricated result is public deception, never a hidden fact. accusers = [p for p, c in self.claims.items() if p in self.alive and p not in self.wolves and any(t in self.wolves and r == 'Werewolf' for t, r in c['reports'].items())] if accusers and len(self.alive) > 3: target = accusers[0] return f'我是预言家。第{self.day}夜查验:{self.label(target)}=狼人。{self.label(target)}的查验说法不可信,今天请投{self.label(target)}。' return f'我倾向投{self.label(target)}。先看发言和票型,不能只凭自称预言家就认定好人;请大家核对查验是否与翻牌一致。' return f'我倾向投{self.label(target)}:{self.reason(target, belief)}。预言家请给出查验对象和结果,大家投票前再核对。' def __call__(self, obs, config=None): raw = self.observe(obs) base = {'day': raw['day'], 'phase': raw['game_state_phase'], 'actor_id': self.me} def action(kind, why, **kw): return {'action_type': kind, 'kwargs': {**base, 'reasoning': why, **kw}} def candidates(name, key): for e in reversed(raw.get('new_player_event_views', [])): if e['event_name'] == name and key in (e.get('data') or {}): return e['data'][key] return [] phase = raw['detailed_phase'] belief = self.belief() if phase == 'DAY_CHAT_AWAIT': return action('ChatAction', '用本座位可见的查验、救援与公开证据发言;声明不是系统认证。', message=self.speech(belief)) if phase == 'DAY_BIDDING_AWAIT': return action('BidAction', '有查验信息时优先发言。', amount=4 if self.inspections else 1) if phase == 'DAY_VOTING_AWAIT': target = self.ballot(self.alive, belief) return action('VoteAction', self.reason(target, belief) if self.role != 'Werewolf' else '结合当前票数集中投给非狼玩家,保护狼队友。', target_id=target) if phase == 'NIGHT_AWAIT_ACTIONS': if self.role == 'Seer': targets = candidates('inspect_request', 'valid_candidates') fresh = [p for p in targets if p not in self.facts] or targets if fresh: target = max(fresh, key=lambda p: (self.claimed(p, 'Seer') + 4 * belief[p] * (1 - belief[p]), self.tie(p))) return action('InspectAction', '优先查验尚无硬证据且能消除争议的玩家,避免重复查验。', target_id=target) if self.role == 'Doctor': targets = candidates('heal_request', 'valid_candidates') if targets: target = max(targets, key=lambda p: (2.8 * self.claimed(p, 'Seer') + (2 if self.facts.get(p) == 'Seer' else 0) + (1 - belief[p]) + (0.4 if p in self.saved else 0), -self.ids.index(p))) return action('HealAction', '在引擎允许的目标中优先保护可信预言家或已确认的好人;此规则允许连续守护。', target_id=target) if self.role == 'Werewolf': targets = candidates('vote_request', 'valid_targets') or [p for p in self.alive if p not in self.wolves] picks = self.night_votes.get(self.day, {}) if targets: target = max(targets, key=lambda p: (4 * sum(t == p for a, t in picks.items() if a != self.me) + 2 * self.claimed(p, 'Doctor') + self.claimed(p, 'Seer'), self.tie(p))) return action('VoteAction', '参考狼队友已投的夜票,优先协同攻击公开特殊身份;不读取其他座位秘密。', target_id=target) return action('NoOpAction', '当前没有本身份可执行的动作。')