Fibonacci-Diplo: How to Adapt Carnage to Online Play

Developers and contributors can find a link to our github page and engage in development project planning here.
Post Reply
Message
Author
nopunin10did
Posts: 35
Joined: Sat Jan 12, 2019 6:17 pm
Karma: 36
Contact:

Fibonacci-Diplo: How to Adapt Carnage to Online Play

#1 Post by nopunin10did » Mon Feb 18, 2019 3:47 pm

Hiyo,

I'm not a webDiplomacy regular, but some of you may have encountered me on PlayDip, the vDip forums, Reddit, or Discord. I'm NoPunIn10Did, and I'm a forum moderator over at PlayDiplomacy. I also do quite a bit of variant development and GM-ing.

One of the projects I've undertaken in the past was to find a way to adapt the popular Carnage scoring system (from face-to-face tournaments) to an online environment. I call it Fibonacci-Diplo, and we're currently using a version of it for a forum/Discord-based tournament this year. It's also gotten the endorsement of Dave Maletsky, the founder of Carnage scoring.

I have a long article about Fibonacci-Diplo Scoring over on the PlayDip forums. Much of that discussion revolves around Elo and a few PlayDip-specific scoring mechanics. However, I thought I'd share some of the basics here, including some pseudocode for calculating score.

Before I get to that, here's the summary.

How Does Carnage Work?
In Carnage, if a player gets a solo, they get 28034 (100% of the points). Everyone else gets 0 points.

If the game ends in a draw (DIAS), players are sorted by rank. Survivors are ranked by SC count, and eliminated players are ranked by year of elimination (later is better).
  • 1st Place: 7000
    2nd Place: 6000
    3rd Place: 5000
    4th Place: 4000
    5th Place: 3000
    6th Place: 2000
    7th Place: 1000
Then each player adds 1 point to their score for each SC they possess. Players tied for a rank split all applicable rank scores. Thus, a 2-way tie for first gives each player (7000+6000)/2 = 6500 points (plus SCs), and the subsequent player gets 3rd place.

Getting Carnage Online?
For the first step of adapting Carnage to online play, remove the SC points component. They're really just a tiebreaker, and they're only necessary in tournaments with small numbers of games. Keep only the rank points (7000-1000) and the solo points (28000).

The second step is generalizing it: in a given game of player count X, grant rank points from 1000 to X*1000 to the players in a draw.

This, on its own, isn't a bad scoring system, but it compares poorly in an environment where other scoring systems are available (particularly DSS). In a seven-player game, the best draw score a player can hope for is the equivalent of a 4-way draw in DSS (7000/28000). It gets worse at higher player counts; in a 17-player game, the board-topper's score would equate to a 9-way draw (17000/153000).

Along Comes Fibonacci
My proposed solution to the scaling issue is to stop using linear scaling (1, 2, 3, 4, etc.) from rank-to-rank and instead scale rank points using the Fibonacci sequence. You can either use a 1-based sequence (1, 1, 2, 3, 5, etc.) or a 0-based sequence (0, 1, 1, 2, 3, 5, etc.). This turns out to scale really well for any game of 5 players or more (4 if you use 1-based), as the highest Fibonacci number in the sequence approaches ~38% of the sum of the whole sequence.

That ultimately means that a sole board-topper will always receive a score roughly equivalent to something between a 2-way and a 3-way draw in DSS. And as with ordinary Carnage, players have substantial incentive to achieve the solo for themselves or prevent the solo for anyone else.

Pseudocode
The following pseudocode is written similar to C# or Java and assumes a 0-based Fibonacci sequence for a minimum of a 5-player game. It provides a normalized score (between 0 and 1) to each player based on their SCs and elimination year. It ranks vacant (surrendered) positions as tied for last.

References to Elo are more Play-Dip specific, but your scoring system is still zero-sum, so it should adapt just as easily to splitting the pot of remaining points.

Code: Select all


public static class FibonacciDrawScoringImplementation {
    
    // Will update the PointsAwardedBeforeElo value on each supplied object in
    // the array.
    // This input should include exactly one object per position in the map.
    // e.g. 
    // - Classic, 1900, or Versailles game should be an array of 7
    // - Ancient Med should be an array of 5
    //
    // Surrendered positions that were never replaced (those that show up as
    // "surrendered" in the site interface) SHOULD be included, with PlayerName
    // set to null.
    //
    // When a player surrenders a position, and another player replaces them,
    // include the REPLACEMENT player here.  Any adjustments based on % of 
    // turns played are assumed to occur after Elo and outside the context of
    // this function.
    public static void ApplyScoresToPlayersInDraw(PlayerPowerData[] players)
    {
        int count = players.Length;
        
        if (count < 5)
        {
            throw new Exception("Fibonacci scoring supports variants " +
            "with five or more players");
        }
        
        // This sorts from least to greatest by SortingScore().  This array
        // contains pointers to the same objects in "players" rather than a
        // deep copy.
        PlayerPowerData[] sortedPlayers = 
            players.OrderBy(p => p.SortingScore());
            
        // To determine the total fibonacci points for a position, we need to
        // calculate all ties.  Therefore, per player, we need the highest and
        // lowest ranks shared.
        
        int currentLowRank = count;
        int currentLowScore = sortedPlayers[0].SortingScore();
        for (int i = 0; i < count; i++)
        {
            if (sortedPlayers[i].SortingScore() != currentLowScore)
            {
                currentLowRank = count - i;
                currentLowScore = sortedPlayers[i].SortingScore();
            }
            sortedPlayers[i].LowestRankShared = currentLowRank;
        }
        
        int currentHighRank = 1;
        int currentHighScore = sortedPlayers[count - 1].SortingScore();
        
        for (int i = count - 1; i >= 0; i--)
        {
            if (sortedPlayers[i].SortingScore() != currentHighScore)
            {
                currentHighRank = count - i;
                currentHighScore = sortedPlayers[i].SortingScore();
            }
            sortedPlayers[i].HighestRankShared = currentHighRank;
        }
        
        decimal[] fibonacciRaw = FibonacciSequenceOfSize(count);
        decimal fibonacciTotal = fibonacciRaw.Sum();
        
        foreach(PlayerPowerData player in sortedPlayers)
        {
            int lowIndex = count - player.LowestRankShared;
            int highIndex = count - player.HighestRankShared;
            
            int numberTied = highIndex - lowIndex + 1;
            
            decimal sumRawForRange = 0.0;
            
            for (int k = lowIndex; k <= highIndex; k++)
            {
                sumRawForRange += fibonacciRaw[k];
            }
            
            // The final pre-Elo score, expressed as a decimal from 0 to 1,
            // is the average of the fibonacci values for ranks shared by
            // this player divided by the total of all fibonacciNumbers.
            player.PointsAwardedBeforeElo = 
                sumRawForRange / numberTied / fibonacciTotal;
        }
        
    }
    
    private static decimal[] FibonacciSequenceOfSize(int size) 
    {
        decimal[] fibonacciNumbers = new decimal[size];
        
        for(int i = 0; i < size; i++)
        {
            if (i == 0)
            {
                fibonacciNumbers[i] = 0.0;
            }
            else if (i == 1 || i == 2)
            {
                fibonacciNumbers[i] = 1.0;
            }
            else
            {
                fibonacciNumbers[i] = 
                    fibonacciNumbers[i-1] + fibonacciNumbers[i-2];
            }
        }
        
        return fibonacciNumbers;
    }
}

// A container for each player / position in the game.
public class PlayerPowerData {
    
    // A Null player name represents an abandoned position
    public string PlayerName {get; set;}
    
    public int SupplyCentersHeld {get; set;}
    
    public int YearEliminated {get; set;}
    
    // Expressed as a decimal from 0 to 1.
    public decimal? PointsAwardedBeforeElo {get; set;}
    
    public int HighestRankShared {get; set;}
    public int LowestRankShared {get; set;}
    
    public bool IsSurrenderedPosition() 
    {
        return string.IsNullOrEmpty(PlayerName);
    }
    
    public int SortingScore()
    {
        // Surrendered positions always place last
        if (IsSurrenderedPosition())
        {
            return int.MinValue;
        }
        
        if (SupplyCentersHeld <= 0)
        {
            return YearEliminated;
        }
        
        // This methodology assumes the year number will never be higher than
        // 10000.
        return SupplyCentersHeld * 10000;
    }
}
Now What?
I don't really have the bandwidth to implement this myself in the php webDip code at this time, as I tend to run out of brainpower for programming at the end of the workday. But, if any of you want to see a Carnage-like system available online that's a feasible alternative to both DSS and SOS, this is a way to do it.
5

Claesar
Posts: 1965
Joined: Tue Oct 03, 2017 10:34 am
Karma: 1490
Contact:

Re: Fibonacci-Diplo: How to Adapt Carnage to Online Play

#2 Post by Claesar » Mon Feb 18, 2019 4:52 pm

That sounds excellent! We coincidentally just had a discussion on the matter and there's definitely interest in a middle ground between SoS and DSS. This sounds like it hits the mark.

Of course, we also have very limited Developer time available so actually getting this online will be a challenge. Let's at least discuss the merits in this thread though! The more interest, the higher the chance someone decides to code this. Theoretically.
3

nopunin10did
Posts: 35
Joined: Sat Jan 12, 2019 6:17 pm
Karma: 36
Contact:

Re: Fibonacci-Diplo: How to Adapt Carnage to Online Play

#3 Post by nopunin10did » Mon Feb 18, 2019 5:39 pm

Some pluses and minuses of Fibonacci / Carnage:

As with SOS, draw size isn't a factor, so player elimination has no inherent value. It may still be necessary, and a power's last SCs might be what a given player needs to consume to increase rank, but it doesn't create the problem whereby achieving a better draw-size requires a strategy that's at odds with attempting the solo (e.g. helping other opponents to swallow up smaller powers just to reduce the surviving player count).

Unlike SOS, a board-topper can't receive quite as many points compared to a solo. The Fibonacci scaling tops out around 38-43%. This is still a sizeable pot to work to achieve, but it doesn't quite diminish the achievement of the solo.

One element of Carnage & Fibonacci that some players don't prefer is that eliminated players can still receive points. In the vast majority of cases, I expect that the points received will still be lower than the "bet" for a given game, so it still represents a loss, but it does mean that there are different shades of losing.

Not everyone cares for having different types of losing; in their minds, a loss-is-a-loss. Maletsky has run Carnage tournaments where all eliminated players tie for last place (to keep all losses equal), but the player feedback showed that maintaining rank-by-year-of-elimination was preferred instead. Fibonacci could easily be adapted to run that way instead (eliminated players tied for last), but I'm not sure if this is actually beneficial.

One minor downside of Fibonacci scoring is that someone cheating with multiple accounts can hypothetically more quickly boost a single player's score than with other DIAS systems. For instance, in a classic game that starts with one real account and six sock puppets, the real player can be assigned Russia and the draw declared immediately to grant that player ~40% of the pot. I expect that you may already have safeguards for this sort of behavior.

A downside of Carnage in face-to-face play is that with very few games, it's really easy to end up with a bunch of players tied for overall tournament scores. This was also a big problem with DSS when it was widely used for F2F play. However, I expect that to matter very little in an online, perpetual-play context.

The driving force of Carnage/Fibonacci, other than to achieve/prevent the solo, is to one-up the player(s) closest to you in rank. It creates incentives to grab SCs from similarly-sized opponents. Unlike SOS, however, a single SC obtained may not always alter your overall score. Games are thus more likely to find a sort of stable-state, as one-dotting a neighbor won't always make a difference.

Like SOS (and unlike DSS), Fibonacci will operate well for a game regardless of whether it does or doesn't have a preset year limit.
5

Claesar
Posts: 1965
Joined: Tue Oct 03, 2017 10:34 am
Karma: 1490
Contact:

Re: Fibonacci-Diplo: How to Adapt Carnage to Online Play

#4 Post by Claesar » Mon Feb 18, 2019 6:44 pm

nopunin10did wrote:
Mon Feb 18, 2019 5:39 pm
...
One minor downside of Fibonacci scoring is that someone cheating with multiple accounts can hypothetically more quickly boost a single player's score than with other DIAS systems. For instance, in a classic game that starts with one real account and six sock puppets, the real player can be assigned Russia and the draw declared immediately to grant that player ~40% of the pot. I expect that you may already have safeguards for this sort of behavior.
...
That's really not a problem :-) It is funny though, to think about.
3

Post Reply

Who is online

Users browsing this forum: No registered users and 35 guests