跳转到内容
菜单
← 返回博客

我们如何计算纸牌游戏中的 Elo 等级分

Illustration of two card players with Elo rating calculations floating above, showing rating change formulas and stats

在竞技纸牌游戏中,平衡等级分并不容易。我们正在为这款受 Sedma 与 Sedmice 启发的游戏开发定制 Elo 系统,并加入特定的表现指标。

Elo 系统有哪些新内容?

动态 K 系数决定等级分变化的速度。Seven 会根据当前等级分调整 K:

  • 低于 1200 Elo:K = 40,新玩家调整更快。
  • 1200–1800 Elo:K = 32,常规调整。
  • 1800–2200 Elo:K = 24,变化更加稳定。
  • 高于 2200 Elo:K = 16,变化非常稳定。

表现奖励:等级分变化不仅取决于胜负。

  • 每次赢得 16 分的局增加 +0.05,最多 +0.15。
  • 每次赢得 24 分的局增加 +0.1,最多 +0.2。
  • 每次获得四分并赢得最后一墩增加 +0.03,最多 +0.09。

失误惩罚:每次错误出牌带来 –0.02 的小幅惩罚,累计最多 –0.3。

奖励调整:如果你输了但发挥不错,奖励仍然生效,不过仅按原数值的 10% 计算。

为什么采用这种方式?

我们希望 Elo 体现你如何获胜或落败,而不仅是结果。输了时的良好表现应该有价值;失误很多、侥幸获胜的对局,则不该过度提高等级分。

public static void CalculateNewRatings(
ref int ratingA,
ref int ratingB,
bool playerAWon,
int badMovesA,
int badMovesB,
int wonBy16A,
int wonBy16B,
int wonBy24A,
int wonBy24B,
int wonBy4AndLastA,
int wonBy4AndLastB
)
{
    // K determines how sensitive the Elo rating is to each match outcome.
    // Higher K means faster rating changes (good for new players),
    // lower K means more stable ratings (good for experienced players).
    int kA = GetKFactor(ratingA);
    int kB = GetKFactor(ratingB);

    // Bonus and malus coefficients
    const float BonusPer16 = 0.05f;
    const float BonusPer24 = 0.1f;
    const float BonusPer4AndLast = 0.03f;
    const float MalusPerBadMove = 0.02f;

    // Caps
    const float MaxBonus16 = 0.15f;
    const float MaxBonus24 = 0.2f;
    const float MaxBonus4AndLast = 0.09f;
    const float MaxMalus = 0.3f;

    float baseScoreA = playerAWon ? 1f : 0f;
    float baseScoreB = 1f - baseScoreA;

    // Bonus for player A (full if won, 1/10 if lost)
    float multiplierA = playerAWon ? 1f : 0.1f;
    float bonusA =
        Mathf.Min(wonBy16A * BonusPer16, MaxBonus16) +
        Mathf.Min(wonBy24A * BonusPer24, MaxBonus24) +
        Mathf.Min(wonBy4AndLastA * BonusPer4AndLast, MaxBonus4AndLast);
    bonusA *= multiplierA;

    // Bonus for player B (full if won, 1/10 if lost)
    float multiplierB = playerAWon ? 0.1f : 1f;
    float bonusB =
        Mathf.Min(wonBy16B * BonusPer16, MaxBonus16) +
        Mathf.Min(wonBy24B * BonusPer24, MaxBonus24) +
        Mathf.Min(wonBy4AndLastB * BonusPer4AndLast, MaxBonus4AndLast);
    bonusB *= multiplierB;

    float malusA = Mathf.Min(badMovesA * MalusPerBadMove, MaxMalus);
    float malusB = Mathf.Min(badMovesB * MalusPerBadMove, MaxMalus);

    float adjustedScoreA = Mathf.Clamp(baseScoreA + bonusA - malusA, 0f, 1f);
    float adjustedScoreB = Mathf.Clamp(baseScoreB + bonusB - malusB, 0f, 1f);

    double expectedA = 1.0 / (1.0 + Math.Pow(10, (ratingB - ratingA) / 400.0));
    double expectedB = 1.0 / (1.0 + Math.Pow(10, (ratingA - ratingB) / 400.0));

    //Debug.Log($"[ELO CALCULATION]");
    //Debug.Log($"Player A: baseScore={baseScoreA}, bonus={bonusA}, malus={malusA}, adjustedScore={adjustedScoreA}, expected={expectedA}");
    //Debug.Log($"Player B: baseScore={baseScoreB}, bonus={bonusB}, malus={malusB}, adjustedScore={adjustedScoreB}, expected={expectedB}");
    //Debug.Log($"Old Ratings - A: {ratingA}, B: {ratingB}");


    ratingA = (int)Math.Round(ratingA + kA * (adjustedScoreA - expectedA));
    ratingB = (int)Math.Round(ratingB + kB * (adjustedScoreB - expectedB));

    //Debug.Log($"New Ratings - A: {ratingA}, B: {ratingB}");
}

private static int GetKFactor(int rating)
{
    if (rating < 1200) return 40;       // Fast adaptation for new players
    if (rating < 1800) return 32;       // Balanced default for most players
    if (rating < 2200) return 24;       // More stable for advanced players
    return 16;                          // Very stable for top-ranked
}

下一步

这个系统仍是实验性方案。我们正在调整系数,也可能改变:

  • 奖励与惩罚的影响程度
  • K 值发生变化的时机
  • 团队对局或平局的处理方式

欢迎分享反馈!你觉得等级分公平吗?有没有觉得某场比赛后的变化不合理?欢迎评论或联系我们,你的意见会帮助完善系统。

Leave a Comment

Scroll to Top