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;
}
}
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.