reputation(玩家信譽實作)¶
本檔角色:玩家信譽(PeerId 層)的實作層 —— 信譽 derive 規則(無獨立事件、由來源事件推導)、derived state、加減分觸發邏輯、新手保護、惡意檢舉者判定、連動權重、API。 設計與分工見 信譽系統.md §1/§2/§4;算式權威見 算式表.md §16–17。對應
src/reputation/。 事件基底BaseEvent/serializeForSigning/consensusNow見 ledger.md。
1. 信譽 = 純 derive(無獨立事件)¶
信譽不開獨立 ledger 事件(同經濟「不另開 mint 事件」)——分數純由來源事件推導:各 delta 在 applyEvent 處理來源事件時算出、累加進 ReputationDerivedState。防偽 = 來源事件本身已驗證(MatchResultEvent multisig/本人單簽 RaceLeaveEvent/仲裁),無人能憑空替第三人塞分。
| 來源 | 推導的信譽 delta(reason) |
|---|---|
MatchResultEvent(multisig) |
match-finish +1(每非斷線完賽者)/ win-streak +5(冠軍)/ frequent-disconnect −5(自 disconnects、僅罰有本場 loadout 者)——加分僅計 finisherCount ≥ 3 場+24h 日額、斷線罰不受限(§5.1) |
RaceLeaveEvent(本人單簽) |
frequent-disconnect −5,只作用 peerId 本人;不改排名/TrueSkill/經濟,且與同場 MatchResult 以 (matchId, peerId) 持久去重 |
ugcUsageStats / ratings(DerivedState) |
ugc-usage-milestone +5(每 100 次)/ high-rated-ugc +20 |
ArbitrationResultEvent |
report-success +10 / copyright-violation −100(含剽竊)/ griefing −20(騷擾、其他-對玩家)/ ugc-blacklisted −20(內容不當、其他-對 UGC)/ malicious-reporter −30 |
DMCA 下架只約束各 provider 自己的供應出口(dmca.md)、不進帳本、不產生任何信譽 delta;provider 案件也不得注入 client-wide derive 或 use-gate。
type ReputationReason =
| "ugc-usage-milestone"
| "match-finish"
| "win-streak"
| "report-success"
| "high-rated-ugc"
| "ugc-blacklisted"
| "frequent-disconnect"
| "griefing"
| "malicious-reporter"
| "copyright-violation";
// 推導出的單筆信譽變動(供 history / UI;**非 ledger 事件**)
interface ReputationDelta {
target: PeerId;
delta: number;
reason: ReputationReason;
causeEventId: EventId;
appliedAt: Timestamp;
}
declare const REPUTATION_DELTAS: Record<ReputationReason, number>; // 數值權威 信譽系統.md §2.2 / §2.3(本檔不重列)
2. Derived State¶
〔MOD-R-053〕 規則查詢必須使用專用持久索引;UI 的最近 100 筆 history 淘汰不得改變共識結果:
interface ReputationDerivedState {
scores: ReadonlyMap<PeerId, number>;
history: ReadonlyMap<PeerId, ReputationDelta[]>; // 最近 100 筆——**僅供 UI 顯示,不得作規則查詢依據**(活躍玩家會擠出舊筆;規則查詢用下方專用索引)
lastMaliciousReporterAt: ReadonlyMap<PeerId, Timestamp>; // 規則索引:惡意檢舉前科(§5.3 30 天 rolling 依據;判罰時寫入)
recentMatchGains: ReadonlyMap<
PeerId,
ReadonlyArray<{ at: Timestamp; delta: number }>
>; // 規則索引:24h 比賽加分窗(§5.1 日額依據;發放時寫入、apply 時修剪 > 24h——日額使其自然 ≤ ~10 筆,可序列化進檢查點)
registeredAt: ReadonlyMap<PeerId, Timestamp>; // = 該 peer 首筆事件「套用時」的 consensusNow(自報 timestamp 倒填無效,ledger.md §6)
matchCount: ReadonlyMap<PeerId, number>; // 僅計「非斷線完賽且 finisherCount ≥ 3」之場(與 match-finish +1 同一計數,信譽系統 §2.2 / §8.1)
ugcCount: ReadonlyMap<PeerId, number>;
ugcMilestonesAwarded: ReadonlyMap<CID, number>; // 規則索引:per-CID 已頒發的使用里程碑數(§5.2 每 100 次 +5 防重複頒發;ugcUsageStats 更新後推導、發放時寫入、可序列化進檢查點)
}
3. 分數計算¶
// deltas = deriveReputationDeltas(peerId, allEvents):掃來源事件(match / 仲裁)+ ugcUsageStats 推導
function computeReputationScore(
peerId: PeerId,
deltas: ReputationDelta[],
): number {
let score = 500; // initial
for (const d of deltas) if (d.target === peerId) score += d.delta;
return Math.max(0, Math.min(1000, score)); // clamp [0,1000]
}
純函數、由事件鏈推導(無獨立信譽事件),權威算式見 算式表.md §16。
4. 新手保護¶
實作 信譽系統.md §4.1 中與 reputation 模組相關的一條(信譽下限 400;仲裁扣分無新手豁免——仲裁加權 > 60% 才成立、本身即防線):
// 註冊 < 7 天 → 信譽下限保護為 400
function applyNewPlayerProtection(
peerId: PeerId,
rawScore: number,
state: ReputationDerivedState,
now: Timestamp, // consensusNow(events),見 ledger.md
): number {
const registeredAt = state.registeredAt.get(peerId);
if (!registeredAt) return rawScore;
const days = (now - registeredAt) / (1000 * 86400);
return days < 7 ? Math.max(rawScore, 400) : rawScore;
}
// 讀點契約:下限保護作用於「分數本身」——scores map 存 raw、所有讀點一律經本函數取分
function getEffectiveScore(
peerId: PeerId,
state: ReputationDerivedState,
now: Timestamp,
): number {
return applyNewPlayerProtection(
peerId,
state.scores.get(peerId) ?? 500,
state,
now,
);
}
〔MOD-R-054〕 讀點契約(信譽系統.md §4.1):仲裁資格與 weight(now = 抽籤檢查點 timestamp)、UGC 評分權重(now = 該 UgcRateEvent.timestamp,ugc-rating.md §6.1)、配對過濾(now = consensusNow)、API getScore / UI——全部讀 getEffectiveScore。consensus derive 內的 now 一律綁事件 / 檢查點 timestamp(決定性、不得用本地時鐘)。
5. 信譽 delta 推導(在 applyEvent 套用來源事件時算)¶
以下皆推導出
ReputationDelta、累加進scores+history,不appendEvent(信譽無獨立事件,§1)。history即推導出的 delta 串(供 UI / 防疊扣查詢)。
5.1 完賽 + 連勝 + 斷線(applyEvent(MatchResultEvent))¶
〔MOD-R-055〕 MatchResult fold 必須共同推導完賽、連勝、斷線信譽 delta,並套用 24h 比賽加分上限:
// matchGains24hOf(p, at):rolling 24h 內((at−24h, at])match-finish + win-streak delta 合計
// —— 自 §2 recentMatchGains 專用索引加總(發放時同步寫入、修剪 > 24h);history 100 筆僅 UI、不得作依據
function deriveMatchReputation(
match: MatchResultEvent,
recentOf: (p: PeerId) => MatchRecord[],
matchGains24hOf: (p: PeerId, at: Timestamp) => number,
): ReputationDelta[] {
const ds: ReputationDelta[] = [],
at = match.timestamp,
cause = match.id;
const forfeited = new Set(match.disconnects.map((d) => d.peerId));
for (const d of match.disconnects) {
// 異常斷線 −5/次——absent(未交 loadout)=無參與證明不罰;有 loadout 者不論 finisherCount 照罰(void 為全體斷線而設,信譽系統 §2.3)
if (match.loadouts[d.peerId] === undefined) continue; // committed 閘:只罰有本場簽章 loadout 者
ds.push({
target: d.peerId,
delta: REPUTATION_DELTAS["frequent-disconnect"],
reason: "frequent-disconnect",
causeEventId: cause,
appliedAt: at,
});
}
const finishers = match.ranking.filter((p) => !forfeited.has(p));
if (finishers.length < 3) return ds; // 經濟 void 場(finisherCount < 3)不發加分(同 經濟系統 §6.2 門檻與理由)
const capped = (d: ReputationDelta) => {
// 24h 日額:已發 + 本場已列 + 本筆 ≤ 上限才發(逐筆檢查、超額不發)
const inMatch = ds
.filter((x) => x.target === d.target && x.delta > 0)
.reduce((s, x) => s + x.delta, 0);
if (
matchGains24hOf(d.target, at) + inMatch + d.delta <=
REPUTATION_MATCH_GAIN_DAILY_MAX
)
ds.push(d);
};
for (const p of finishers)
// 完賽 +1(每非斷線完賽者;先於連勝檢查)
capped({
target: p,
delta: REPUTATION_DELTAS["match-finish"],
reason: "match-finish",
causeEventId: cause,
appliedAt: at,
});
const winner = match.ranking[0]; // 連勝看冠軍(總名次第 1)
const recent = recentOf(winner); // 最近 5 場「含本場」、僅列 finisherCount ≥ 3 之場(void 不入窗、不破連勝)
if (recent.length === 5 && recent.every((r) => r.ranking[0] === winner))
// 連勝:最近 5 場(含本場)總名次全部第 1
capped({
target: winner,
delta: REPUTATION_DELTAS["win-streak"],
reason: "win-streak",
causeEventId: cause,
appliedAt: at,
});
return ds;
}
比賽加分日額 REPUTATION_MATCH_GAIN_DAILY_MAX(10,rolling 24h、非日曆日;protocol.md §5)只蓋 match-finish / win-streak——扣分與其他加分不經此額度(權威 信譽系統.md §2.2)。
5.2 UGC 使用門檻(每 100 次 +5;ugcUsageStats 更新後推導)¶
// cause / at = 觸發 ugcUsageStats 更新的來源事件(MatchResultEvent)——appliedAt 綁事件、跨 peer history 一致(不得用 derivedAt)
function deriveMilestoneReputation(
cid: CID,
state: DerivedState,
cause: EventId,
at: Timestamp,
): ReputationDelta[] {
const stats = state.ugcUsageStats.get(cid);
const author = state.ugcRecords.get(cid)?.author;
if (!stats || !author) return [];
const milestone = Math.floor(stats.totalUses / 100);
const awarded = state.ugcMilestonesAwarded.get(cid) ?? 0; // 防重複頒發(推導旗標)
return Array.from({ length: Math.max(0, milestone - awarded) }, () => ({
target: author,
delta: REPUTATION_DELTAS["ugc-usage-milestone"],
reason: "ugc-usage-milestone",
causeEventId: cause,
appliedAt: at,
}));
}
5.3 惡意檢舉者判定(三條件 AND,防誤殺;applyEvent(ArbitrationResultEvent))¶
〔MOD-R-056〕 對齊 信譽系統.md §4.3:樣本足夠 + 準確率低 + 無前科保護,三條件同時滿足才 −30。
// arb = 觸發判定的 ArbitrationResultEvent(reject 結案時呼叫)——cause / appliedAt 綁事件、跨 peer 一致
function deriveMaliciousReporter(
reporter: PeerId,
state: DerivedState,
arb: ArbitrationResultEvent,
): ReputationDelta[] {
const acc = state.reportAccuracy.get(reporter);
if (!acc || acc.reports < 10) return []; // ① 樣本不足(已結案 < 10 筆;準確率 = lifetime 累積)
if (acc.successful * 10 >= acc.reports * 3) return []; // ② 準確率 ≥ 30%(整數交叉相乘、免浮點)
const lastAt = state.lastMaliciousReporterAt.get(reporter); // 專用規則索引(§2;history 100 筆僅 UI、會被活躍玩家擠出)
if (lastAt !== undefined && lastAt >= arb.timestamp - 30 * 86400 * 1000)
return []; // ③ 30 天內已判過 → 不疊扣(rolling);判罰時同步寫入索引
return [
{
target: reporter,
delta: REPUTATION_DELTAS["malicious-reporter"],
reason: "malicious-reporter",
causeEventId: arb.id,
appliedAt: arb.timestamp,
},
];
}
5.4 檢舉成立(applyEvent(ArbitrationResultEvent),罰則依檢舉類型)¶
〔MOD-R-057〕 對齊 信譽系統.md §2.3 / §5.3。「其他」型無專屬 reason,借對象所屬桶的標籤(對 UGC → ugc-blacklisted、對玩家 → griefing);reject / 流局 / 抽籤前撤回不產生 delta(只進 / 不進準確率統計見 moderation.md §5.4)。無新手豁免。
function deriveArbitrationReputation(
arb: ArbitrationResultEvent,
report: ReportEvent,
state: DerivedState,
): ReputationDelta[] {
if (arb.result !== "pass") return []; // reject → 零 delta(僅準確率統計 → §5.3)
const at = arb.timestamp,
cause = arb.id,
ds: ReputationDelta[] = [];
ds.push({
target: report.reporter,
delta: REPUTATION_DELTAS["report-success"],
reason: "report-success",
causeEventId: cause,
appliedAt: at,
});
const offender =
report.targetKind === "cid" // 責任人:UGC 定罪 → 作者
? state.ugcRecords.get(report.target as CID)!.author
: (report.target as PeerId);
const reason: ReputationReason =
report.reason === "copyright-violation"
? "copyright-violation" // 含剽竊(併入版權)−100
: report.targetKind === "cid"
? "ugc-blacklisted" // 內容不當 / 其他-對 UGC −20
: "griefing"; // 騷擾 / 其他-對玩家 −20
ds.push({
target: offender,
delta: REPUTATION_DELTAS[reason],
reason,
causeEventId: cause,
appliedAt: at,
});
return ds;
}
CID 定罪同時觸發 BlacklistEvent(下架);同一責任人定罪累積 ≥ 3 → 全域玩家黑名單(純 derive 三振,見 moderation.md §5.5)。
6. 連動 API¶
6.1 仲裁投票權重¶
〔MOD-R-010〕
// 整數預算表(共識計算禁浮點 log10——tally 全網重算驗證,跨硬體 libm 差異會翻轉 60% 邊界案;算式表 §17)
function arbitrationVoteWeightX100(reputation: number): number {
return ARBITRATION_WEIGHT_X100_TABLE[Math.max(0, Math.min(1000, reputation))];
// 表 = 離線一次性生成 round(100 × log10(rep + 10))、隨 client 出貨(bit-exact);rep ∈ [0,1000] → weightX100 ∈ [100, 300]
}
權威算式見 算式表.md §17。檢舉與仲裁採純仲裁模型(隨機抽合格仲裁者投票、加權通過 > 60% 才黑名單),見 moderation.md 與 檢舉與仲裁.md。
註:
report-success +10由仲裁通過觸發、非加權門檻(所有檢舉走仲裁,見 moderation.md)。UGC 評分權重ratingWeightX100(50/100/150)是另一回事,見 ugc-rating.md § 評分權重。
6.2 配對過濾¶
function shouldFilterByReputation(
threshold: number,
state: DerivedState,
now: Timestamp,
): (p: PeerId) => boolean {
return (peerId) =>
getEffectiveScore(peerId, state.reputation, now) >= threshold; // 保護後分數(§4 讀點契約;now = consensusNow)
}
7. UI 顯示¶
function reputationToStars(score: number): number {
// 0-1000 → 0-5 ★
return Math.floor(score / 200);
}
| 信譽 | 星級 |
|---|---|
| 0~199 | 0★(無星) |
| 200~399 | ⭐ |
| 400~599 | ⭐⭐ |
| 600~799 | ⭐⭐⭐ |
| 800~999 | ⭐⭐⭐⭐ |
| 1000 | ⭐⭐⭐⭐⭐ |
信譽變動歷史 UI 見 信譽系統.md §9.2。
8. 公開 API¶
interface ReputationApi {
getScore(peerId: PeerId): Promise<number>;
getStars(peerId: PeerId): Promise<number>;
getHistory(peerId: PeerId, limit: number): Promise<ReputationDelta[]>;
onScoreChange(
peerId: PeerId,
handler: (newScore: number) => void,
): Unsubscribe;
getDistribution(): Promise<ReputationDistribution>;
}
interface ReputationDistribution {
total: number;
bands: Array<{ min: number; max: number; count: number }>;
median: number;
}
9. 風險與緩解¶
| 風險 | 緩解 |
|---|---|
| 信譽通膨 | 上限 1000 + 加分事件門檻 |
| 新人被惡意刷低 | 新人保護期 7 天保底 400 + 純仲裁定罪門檻(加權 > 60%)+ 惡意檢舉者 −30 反制 |
| 計算錯誤 | 純函數 + 帳本檢查點(見 ledger.md) |
| Sybil 刷分(合謀完賽 / 連勝農場) | 比賽加分僅計 finisherCount ≥ 3 場 + rolling 24h 日額(§5.1)+ 多方簽章;殘餘風險 = honest-majority 假設 |