πŸ“Š How We Calculate Elo Ratings in Our Card Game

Balancing player ratings in a competitive card game isn’t easy β€” and we’ve been iterating on a custom Elo system that suits Sedma, Semice-inspired gameplay with unique performance metrics.

🎯 What’s new in our Elo system?

βœ… Dynamic K-factor
The K-factor controls how fast ratings adjust. Our game adapts K based on your current rating:

  • <1200 Elo: K = 40 (new players adjust faster)
  • 1200–1800 Elo: K = 32 (normal adjustment)
  • 1800–2200 Elo: K = 24 (more stable)
  • >2200 Elo: K = 16 (very stable)

βœ… Performance-based bonuses
Your Elo update isn’t just win/loss:

  • +0.05 per round won by 16 points (max +0.15)
  • +0.1 per round won by 24 points (max +0.2)
  • +0.03 per round won with 4 and last (max +0.09)

βœ… Bad move penalties
Each bad move applies a small malus (–0.02), capped at –0.3 total.

βœ… Bonus scaling
If you lose but played well? Your bonuses still apply β€” but at 10% strength.


πŸ’‘ Why this approach?

We want Elo to reflect how you win (or lose), not just the result. A strong performance in a loss should still count for something. Similarly, reckless wins with many bad moves shouldn’t inflate ratings.


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
}

πŸ“ What’s next?

⚠️ This system is experimental!
We’re actively tuning the coefficients and may adjust:

  • How much bonuses/maluses weigh
  • When K values change
  • How to handle team matches or draws

πŸ’¬ We want your feedback!
Does this rating feel fair? Did a match leave you thinking the rating change wasn’t justified?
πŸ‘‰ Comment below or reach out β€” your input helps us refine the system!


Leave a Comment

Scroll to Top