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

**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.