Compare commits

..

21 Commits

Author SHA1 Message Date
770c4dbcc6 Documentation warnings from pandoc resolved 2025-11-26 13:59:49 +00:00
d7a60176bb Library type. Requirement update 2025-11-20 13:35:45 +00:00
60102910e3 Regular update 2025-09-02 09:41:15 +00:00
dd4ecff60a Update packages 2025-08-27 08:26:23 +00:00
03a9a3987a Less modern PHP to make the older linting tools work on the examples code 2025-07-14 11:59:21 +00:00
ec2e637315 Warning removal 2025-07-08 09:41:38 +00:00
1d06fa0b0a Requirement update 2025-06-24 12:55:00 +00:00
a097fa7f71 Few more comments 2025-06-18 11:49:40 +00:00
ad9d7f94de Upodate PHPUnit to latest version 2025-06-06 14:24:46 +00:00
1725979b1c Bumping versions 2025-06-02 08:49:46 +00:00
ac7e3f5c2d CodeStandard as HTML / MarkDown 2025-05-14 09:28:51 +00:00
adc587ac0d More coding standards.
Diagrams in API documentation
2025-05-13 12:46:11 +00:00
d729caac1f Simplifying some casts 2025-05-12 09:40:44 +00:00
bd9fccb87b Regular updates and fixing static analysis findings 2025-05-12 09:19:17 +00:00
55656b7889 More standards 2025-04-29 13:21:47 +00:00
38f332b223 Continuing the process of making this modern 2025-04-25 09:22:08 +00:00
b38a9656eb Much stricter coding standards for phpstan, phpstan and psalm. 2025-04-15 08:14:08 +00:00
3c617e9869 Move 2025-04-01 11:53:37 +00:00
5a414b8307 More warnings from Psalm. Reduced Psalm config size 2025-01-28 09:38:14 +00:00
e2620fde53 PHP8.3 style overrides added 2025-01-28 09:20:03 +00:00
22002891c5 Upgrade to PHP8.4 2025-01-28 09:18:50 +00:00
80 changed files with 568 additions and 2807 deletions

5
.gitignore vendored
View File

@@ -1,6 +1,5 @@
.*/
vendor
vendor/
.*.cache/
*.phar
.phpdoc/
output/
tools/

12
.phive/phars.xml Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phive xmlns="https://phar.io/phive">
<phar name="phpmd" version="^2.15.0" installed="2.15.0" location="./tools/phpmd" copy="false"/>
<phar name="phpstan" version="^2.1.12" installed="2.1.32" location="./tools/phpstan" copy="false"/>
<phar name="psalm" version="^7.0.0-beta6" installed="7.0.0-beta11" location="./tools/psalm" copy="false"/>
<phar name="phpcs" version="^3.12.2" installed="3.13.5" location="./tools/phpcs" copy="false"/>
<phar name="phpcbf" version="^3.12.2" installed="3.13.5" location="./tools/phpcbf" copy="false"/>
<phar name="phpdocumentor" version="^3.7.1" installed="3.9.1" location="./tools/phpdocumentor" copy="false"/>
<phar name="phpbench" version="^1.4.1" installed="1.4.3" location="./tools/phpbench" copy="false"/>
<phar name="infection" version="^0.29.14" installed="0.29.14" location="./tools/infection" copy="false"/>
<phar name="phpunit" version="^12.1.3" installed="12.4.4" location="./tools/phpunit" copy="false"/>
</phive>

View File

@@ -6,6 +6,11 @@
<file>tests/</file>
<file>benchmark/</file>
<arg name="basepath" value="."/>
<arg name="colors"/>
<arg name="parallel" value="8"/>
<arg name="report" value="emacs"/>
<arg value="p"/>
<rule ref="PSR1">
<exclude name="Generic.Files.LineLength"/>
@@ -18,23 +23,19 @@
<exclude name="Generic.Files.LowercasedFilename.NotFound"/>
<exclude name="Generic.PHP.ClosingPHPTag.NotFound"/>
<exclude name="Generic.Files.EndFileNoNewline.Found"/>
<exclude name="Generic.Files.EndFileNoNewline.Found"/>
<exclude name="Generic.Arrays.DisallowShortArraySyntax.Found"/>
<exclude name="Generic.Functions.OpeningFunctionBraceKernighanRitchie.BraceOnNewLine"/>
<exclude name="Generic.Classes.OpeningBraceSameLine.BraceOnNewLine"/>
<exclude name="Generic.PHP.LowerCaseConstant.Found"/>
<exclude name="Generic.Formatting.SpaceAfterCast"/>
<exclude name="Generic.Formatting.MultipleStatementAlignment.NotSameWarning"/>
<exclude name="Generic.Commenting.DocComment.MissingShort"/>
<exclude name="Generic.NamingConventions.AbstractClassNamePrefix.Missing"/>
<exclude name="Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed"/>
<exclude name="Generic.NamingConventions.InterfaceNameSuffix.Missing"/>
<exclude name="Generic.Commenting.Todo.TaskFound"/>
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUse"/>
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed"/>
<exclude name="Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed"/>
<exclude name="Generic.Formatting.SpaceBeforeCast.NoSpace"/>
<exclude name="Generic.CodeAnalysis.UselessOverridingMethod.Found"/>
<exclude name="Squiz.Functions.MultiLineFunctionDeclaration.NewlineBeforeOpenBrace"/>
</rule>
@@ -50,4 +51,25 @@
</property>
</properties>
</rule>
<rule ref="Generic.Formatting.SpaceAfterCast">
<properties>
<property name="spacing" value="0"/>
</properties>
</rule>
<!-- Do not allow unreachable code. -->
<rule ref="Squiz.PHP.NonExecutableCode"/>
<!-- Do not allow ambiguous conditions. -->
<rule ref="Generic.CodeAnalysis.RequireExplicitBooleanOperatorPrecedence"/>
<!-- The testing bootstrap file uses string concats to stop IDEs seeing the class aliases -->
<rule ref="Generic.Strings.UnnecessaryStringConcat" />
<!-- This test file specifically *needs* Windows line endings for testing purposes. -->
<rule ref="Generic.Files.LineEndings.InvalidEOLChar" />
<!-- Avoid false positive with this sniff detecting itself -->
<rule ref="Generic.Commenting.Todo"/>
</ruleset>

View File

@@ -1,12 +0,0 @@
path:
- src/
- tests/
- benchmark/
jobs: 10
extensions:
- php
exclude:
- vendor
warning: true
memory-limit: -1
log-junit: "output/lint.xml"

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"php.version": "8.4"
}

View File

@@ -1,5 +1,6 @@
---
title: README
lang: en
...
# PHP TrueSkill Implementation
This is a PHP port of the Moserware.Skills project that's available at

View File

@@ -14,7 +14,7 @@ use DNW\Skills\Team;
/**
* Basic Benchmarks.
*/
class BasicBench
final class BasicBench
{
/**
* To benchmark performance when using TwoPlayerTrueSkillCalculator
@@ -42,8 +42,10 @@ class BasicBench
$team1 = new Team($p1, $newRatings->getRating($p1));
$team2 = new Team($p2, $newRatings->getRating($p2));
$newRatings->getRating($p1)->getConservativeRating();
$newRatings->getRating($p2)->getConservativeRating();
ob_start();
echo $newRatings->getRating($p1)->getConservativeRating();
echo $newRatings->getRating($p2)->getConservativeRating();
ob_clean();
}
}
@@ -73,8 +75,10 @@ class BasicBench
$team1 = new Team($p1, $newRatings->getRating($p1));
$team2 = new Team($p2, $newRatings->getRating($p2));
$newRatings->getRating($p1)->getConservativeRating();
$newRatings->getRating($p2)->getConservativeRating();
ob_start();
echo $newRatings->getRating($p1)->getConservativeRating();
echo $newRatings->getRating($p2)->getConservativeRating();
ob_clean();
}
}
@@ -104,8 +108,10 @@ class BasicBench
$team1 = new Team($p1, $newRatings->getRating($p1));
$team2 = new Team($p2, $newRatings->getRating($p2));
$newRatings->getRating($p1)->getConservativeRating();
$newRatings->getRating($p2)->getConservativeRating();
ob_start();
echo $newRatings->getRating($p1)->getConservativeRating();
echo $newRatings->getRating($p2)->getConservativeRating();
ob_clean();
}
}
@@ -138,9 +144,11 @@ class BasicBench
$team2 = new Team($p2, $newRatings->getRating($p2));
$team3 = new Team($p3, $newRatings->getRating($p3));
$newRatings->getRating($p1)->getConservativeRating();
$newRatings->getRating($p2)->getConservativeRating();
$newRatings->getRating($p3)->getConservativeRating();
ob_start();
echo $newRatings->getRating($p1)->getConservativeRating();
echo $newRatings->getRating($p2)->getConservativeRating();
echo $newRatings->getRating($p3)->getConservativeRating();
ob_clean();
}
}
}

View File

@@ -1,16 +1,19 @@
{
"name": "dnw/php-trueskill",
"description": "Trueskill implementation by Moserware updated for PHP 8.2",
"keywords": ["trueskill", "matchmaking", "ranking", "skill", "elo"],
"description": "Trueskill implementation by Moserware updated for PHP 8.4",
"keywords": [
"trueskill",
"matchmaking",
"ranking",
"skill",
"elo"
],
"require": {
"php": "^8.2"
"php": "^8.4"
},
"require-dev": {
"phpstan/phpstan": "^1.0",
"vimeo/psalm": "dev-master",
"phpmetrics/phpmetrics": "^3.0-dev",
"phpunit/phpunit": "^11.2",
"psalm/plugin-phpunit": "^0.18.4",
"rector/rector": "^1.0",
"league/csv": "^9.0"
},
@@ -25,27 +28,27 @@
}
},
"scripts": {
"test": "phpunit",
"document": "phpDocumentor",
"benchmark": "phpbench run --report=default --output=build-artifact",
"metrics": "vendor/bin/phpmetrics --config=phpmetrics.yml",
"test": "tools/phpunit",
"document": "tools/phpdocumentor",
"benchmark": "tools/phpbench run --report=default --output=build-artifact",
"metrics": "phpmetrics --config=phpmetrics.yml",
"lint": [
"phplint",
"phpcs",
"phpcbf src/ tests/ benchmark/ examples/",
"phpmd src/,tests/,benchmark/,examples/ text phpmd.ruleset.xml"
"tools/phpcbf",
"tools/phpcs",
"tools/phpmd src/,tests/,benchmark/,examples/ text phpmd.ruleset.xml"
],
"analyze": [
"@analyze-phpstan",
"@analyze-psalm",
"@analyze-rector"
"@analyze-phpstan",
"@analyze-psalm",
"@analyze-rector"
],
"analyze-phpstan":"vendor/bin/phpstan analyze --error-format=raw",
"analyze-psalm": "vendor/bin/psalm --no-cache --show-info=true",
"analyze-rector": "vendor/bin/rector --dry-run",
"analyze-phpstan": "tools/phpstan analyze --error-format=raw",
"analyze-psalm": "tools/psalm --show-info=true",
"analyze-rector": "rector --dry-run",
"html": [
"pandoc -s README.md -o output/README.html",
"pandoc -s docs/index.rst -o output/index.html"
"pandoc --verbose -s README.md -o output/README.html",
"pandoc --verbose -f rst -s docs/index.rst -o output/index.html --metadata=lang:en",
"tools/phpcs --generator=MarkDown | pandoc --verbose -f markdown -s -o output/CodeStandard.html --metadata title='Code Standard' --metadata=lang:en"
],
"all": [
"@test",
@@ -55,5 +58,6 @@
"@metrics",
"@html"
]
}
},
"type": "library"
}

2787
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
=============
Documentation
=============
This is a PHP port of the Moserware.Skills project that's available at
http://github.com/moserware/Skills
@@ -17,9 +19,9 @@ https://www.moserware.com/2010/03/computing-your-skill.html
From Microsoft
--------------
The TrueSkill ranking system is a skill based ranking system for Xbox Live(opens in new tab) developed at Microsoft Research(opens in new tab). The purpose of a ranking system is to both identify and track the skills of gamers in a game (mode) in order to be able to match them into competitive matches. TrueSkill has been used to rank and match players in many different games, from Halo 3 to Forza Motorsport 7(opens in new tab).
The TrueSkill ranking system is a skill based ranking system for Xbox Live developed at Microsoft Research. The purpose of a ranking system is to both identify and track the skills of gamers in a game (mode) in order to be able to match them into competitive matches. TrueSkill has been used to rank and match players in many different games, from Halo 3 to Forza Motorsport 7.
An improved version of the TrueSkill ranking system, named TrueSkill 2(opens in new tab), launched with Gears of War 4(opens in new tab) and was later incorporated into Halo 5(opens in new tab).
An improved version of the TrueSkill ranking system, named TrueSkill 2, launched with Gears of War 4 and was later incorporated into Halo 5.
The classic TrueSkill ranking system only uses the final standings of all teams in a match in order to update the skill estimates (ranks) of all players in the match. The TrueSkill 2 ranking system also uses the individual scores of players in order to weight the contribution of each player to each team. As a result, TrueSkill 2 is much faster at figuring out the skill of a new player.
@@ -33,11 +35,16 @@ Links
* `Test report <test/index.html>`_
* `Mutation testing <mutation/infection.html>`_
* `Code metrics <metrics/index.html>`_
* `Code Standard <CodeStandard.html>`_
* `Benchmark <benchmark.html>`_
Standard Tools
--------------
* PHP8.3
* PHP8.4
Development Tools
-------------------
* PlantUML
* GraphViz
* Pandoc
@@ -47,7 +54,5 @@ PHP Tools
---------
For development Composer and the following packages are used (Recommended as Phars installed via Phive)
* sudo phive install -g composer phpdocumentor infection phpcs phpcbf phploc phpbench
* sudo phive install -g overtrue/phplint --force-accept-unsigned
* composer install
* composer all

View File

@@ -11,13 +11,14 @@ use DNW\Skills\Team;
use DNW\Skills\Teams;
//load the CSV document from a stream
$stream = fopen('motogp.csv', 'r');
$csv = Reader::createFromStream($stream);
$csv = Reader::createFromPath('motogp.csv', 'r');
$csv->setDelimiter(',');
$csv->setHeaderOffset(0);
$csv->setEscape('');
//build a statement
$stmt = Statement::create()->where(static fn (array $record): bool => $record['category'] == "MotoGP" || $record['category'] == "500cc");
$statement = new Statement();
$stmt = $statement->where(static fn (array $record): bool => $record['category'] == "MotoGP" || $record['category'] == "500cc");
/**
* @var $riders Player[]

View File

@@ -16,5 +16,5 @@
</source>
</api>
</version>
<!--setting name="graphs.enabled" value="true"/-->
<setting name="graphs.enabled" value="true"/>
</phpdocumentor>

View File

@@ -12,11 +12,5 @@
<directory name="src"/>
<directory name="tests"/>
<directory name="benchmark"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<plugins>
<pluginClass class="Psalm\PhpUnitPlugin\Plugin"/>
</plugins>
</psalm>

View File

@@ -24,7 +24,7 @@ abstract class FactorGraphLayer
*/
private array $inputVariablesGroups = [];
protected function __construct(private readonly TrueSkillFactorGraph $parentFactorGraph)
public function __construct(private readonly TrueSkillFactorGraph $parentFactorGraph)
{
}

View File

@@ -7,7 +7,7 @@ namespace DNW\Skills\FactorGraphs;
/**
* Helper class for computing the factor graph's normalization constant.
*/
class FactorList
final class FactorList
{
/**
* @var Factor[] $list
@@ -31,11 +31,11 @@ class FactorList
$numberOfMessages = $factor->getNumberOfMessages();
for ($j = 0; $j < $numberOfMessages; ++$j) {
$sumLogZ += $factor->sendMessageIndex($j);
$sumLogZ += (float)$factor->sendMessageIndex($j);
}
}
$sumLogS = 0;
$sumLogS = 0.0;
foreach ($list as &$currentFactor) {
$sumLogS += $currentFactor->getLogNormalization();

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace DNW\Skills\FactorGraphs;
class KeyedVariable extends Variable
final class KeyedVariable extends Variable
{
public function __construct(private readonly mixed $key, mixed $prior)
{

View File

@@ -6,7 +6,7 @@ namespace DNW\Skills\FactorGraphs;
use DNW\Skills\Numerics\GaussianDistribution;
class Message
final class Message
{
public function __construct(private GaussianDistribution $value)
{

View File

@@ -4,12 +4,13 @@ declare(strict_types=1);
namespace DNW\Skills\FactorGraphs;
class ScheduleLoop extends Schedule
final class ScheduleLoop extends Schedule
{
public function __construct(private readonly Schedule $scheduleToLoop, private readonly float $maxDelta)
{
}
#[\Override]
public function visit(int $depth = -1, int $maxDepth = 0): float
{
$delta = $this->scheduleToLoop->visit($depth + 1, $maxDepth);

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace DNW\Skills\FactorGraphs;
class ScheduleSequence extends Schedule
final class ScheduleSequence extends Schedule
{
/**
* @param Schedule[] $schedules
@@ -13,6 +13,7 @@ class ScheduleSequence extends Schedule
{
}
#[\Override]
public function visit(int $depth = -1, int $maxDepth = 0): float
{
$maxDelta = 0;

View File

@@ -4,12 +4,13 @@ declare(strict_types=1);
namespace DNW\Skills\FactorGraphs;
class ScheduleStep extends Schedule
final class ScheduleStep extends Schedule
{
public function __construct(private readonly Factor $factor, private readonly int $index)
{
}
#[\Override]
public function visit(int $depth = -1, int $maxDepth = 0): float
{
return $this->factor->updateMessageIndex($this->index);

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace DNW\Skills\FactorGraphs;
class VariableFactory
final readonly class VariableFactory
{
public function __construct(private readonly \Closure $varPriorInitializer)
public function __construct(private \Closure $varPriorInitializer)
{
}

View File

@@ -7,30 +7,30 @@ namespace DNW\Skills;
/**
* Parameters about the game for calculating the TrueSkill.
*/
class GameInfo
final readonly class GameInfo
{
/**
* Default initial mean / 6
*/
private const DEFAULT_BETA = 4.1666666666666666666666666666667;
private const float DEFAULT_BETA = 4.1666666666666666666666666666667;
private const DEFAULT_DRAW_PROBABILITY = 0.10;
private const float DEFAULT_DRAW_PROBABILITY = 0.10;
/**
* Default initial mean / 300
*/
private const DEFAULT_DYNAMICS_FACTOR = 0.083333333333333333333333333333333;
private const float DEFAULT_DYNAMICS_FACTOR = 0.083333333333333333333333333333333;
private const DEFAULT_INITIAL_MEAN = 25.0;
private const float DEFAULT_INITIAL_MEAN = 25.0;
private const DEFAULT_INITIAL_STANDARD_DEVIATION = 8.3333333333333333333333333333333;
private const float DEFAULT_INITIAL_STANDARD_DEVIATION = 8.3333333333333333333333333333333;
public function __construct(
private readonly float $initialMean = self::DEFAULT_INITIAL_MEAN,
private readonly float $initialStdDev = self::DEFAULT_INITIAL_STANDARD_DEVIATION,
private readonly float $beta = self::DEFAULT_BETA,
private readonly float $dynamicsFactor = self::DEFAULT_DYNAMICS_FACTOR,
private readonly float $drawProbability = self::DEFAULT_DRAW_PROBABILITY
private float $initialMean = self::DEFAULT_INITIAL_MEAN,
private float $initialStdDev = self::DEFAULT_INITIAL_STANDARD_DEVIATION,
private float $beta = self::DEFAULT_BETA,
private float $dynamicsFactor = self::DEFAULT_DYNAMICS_FACTOR,
private float $drawProbability = self::DEFAULT_DRAW_PROBABILITY
)
{
}

View File

@@ -11,7 +11,7 @@ use Exception;
*
* @see http://www.moserware.com/2008/01/borrowing-ideas-from-3-interesting.html
*/
class Guard
final class Guard
{
public static function argumentIsValidIndex(int $index, int $count, string $parameterName): void
{
@@ -23,7 +23,7 @@ class Guard
public static function argumentInRangeInclusive(float $value, float $min, float $max, string $parameterName): void
{
if (($value < $min) || ($value > $max)) {
throw new Exception($parameterName . ' is not in the valid range [' . $min . ', ' . $max . ']');
throw new Exception($parameterName . ' is not in the valid range [' . (int)$min . ', ' . (int)$max . ']');
}
}
}

View File

@@ -7,15 +7,15 @@ namespace DNW\Skills;
/**
* Basic hashmap that supports object keys.
*/
class HashMap
final class HashMap
{
/**
* @var mixed[] $hashToValue
* @var mixed[] $hashToValue Store the hash to value mapping.
*/
private array $hashToValue = [];
/**
* @var mixed[] $hashToKey
* @var mixed[] $hashToKey Store the hash to original key mapping.
*/
private array $hashToKey = [];

View File

@@ -10,7 +10,7 @@ namespace DNW\Skills\Numerics;
* @author Jeff Moser <jeff@moserware.com>
* @copyright 2010 Jeff Moser
*/
class BasicMath
final class BasicMath
{
/**
* Squares the input (input^2 = input * input)
@@ -27,7 +27,7 @@ class BasicMath
/**
* Sums the items in $itemsToSum
*
* @param mixed[] $itemsToSum The items to sum,
* @param mixed[] $itemsToSum The items to sum.
* @param \Closure $callback The function to apply to each array element before summing.
*
* @return float The sum.

View File

@@ -10,11 +10,11 @@ namespace DNW\Skills\Numerics;
* @author Jeff Moser <jeff@moserware.com>
* @copyright 2010 Jeff Moser
*/
class GaussianDistribution
final class GaussianDistribution
{
private const DEFAULT_STANDARD_DEVIATION = 1.0;
private const float DEFAULT_STANDARD_DEVIATION = 1.0;
private const DEFAULT_MEAN = 0.0;
private const float DEFAULT_MEAN = 0.0;
/**
* Square Root 2π.
@@ -23,7 +23,7 @@ class GaussianDistribution
*
* @link https://www.wolframalpha.com/input?i=sqrt%282*pi%29 Source of value
*/
private const M_SQRT_2_PI = 2.5066282746310005024157652848110452530069867406099383166299235763;
private const float M_SQRT_2_PI = 2.5066282746310005024157652848110452530069867406099383166299235763;
/**
* Log of Square Root 2π.
@@ -32,10 +32,11 @@ class GaussianDistribution
*
* @link https://www.wolframalpha.com/input?i=log%28sqrt%282*pi%29%29 Source of value
*/
private const M_LOG_SQRT_2_PI = 0.9189385332046727417803297364056176398613974736377834128171515404;
private const float M_LOG_SQRT_2_PI = 0.9189385332046727417803297364056176398613974736377834128171515404;
// precision and precisionMean are used because they make multiplying and dividing simpler
// (see the accompanying math paper for more details)
/**
* Precision and precisionMean are used because they make multiplying and dividing simpler.
*/
private float $precision = 1.0;
private float $precisionMean = 0.0;
@@ -171,7 +172,7 @@ class GaussianDistribution
$meanDifference = $numerator->mean - $denominator->mean;
return log($denominator->variance) + self::M_LOG_SQRT_2_PI - log($varianceDifference) / 2.0 +
BasicMath::square($meanDifference) / (2 * $varianceDifference);
BasicMath::square($meanDifference) / (2.0 * $varianceDifference);
}
public static function at(float $var, float $mean = 0.0, float $standardDeviation = 1.0): float
@@ -182,7 +183,7 @@ class GaussianDistribution
// stdDev * sqrt(2*pi)
$multiplier = 1.0 / ($standardDeviation * self::M_SQRT_2_PI);
$expPart = exp((-1.0 * BasicMath::square($var - $mean)) / (2 * BasicMath::square($standardDeviation)));
$expPart = exp((-1.0 * BasicMath::square($var - $mean)) / (2.0 * BasicMath::square($standardDeviation)));
return $multiplier * $expPart;
}
@@ -200,7 +201,7 @@ class GaussianDistribution
$z = abs($var);
$t = 2.0 / (2.0 + $z);
$ty = 4 * $t - 2;
$ty = 4.0 * $t - 2.0;
$coefficients = [
-1.3026537197817094,
@@ -259,8 +260,8 @@ class GaussianDistribution
return 100;
}
$pp = ($p < 1.0) ? $p : 2 - $p;
$t = sqrt(-2 * log($pp / 2.0)); // Initial guess
$pp = ($p < 1.0) ? $p : 2.0 - $p;
$t = sqrt(-2.0 * log($pp / 2.0)); // Initial guess
$x = -M_SQRT1_2 * ((2.30753 + $t * 0.27061) / (1.0 + $t * (0.99229 + $t * 0.04481)) - $t);
for ($j = 0; $j < 2; ++$j) {
@@ -274,6 +275,6 @@ class GaussianDistribution
public static function inverseCumulativeTo(float $var, float $mean = 0.0, float $standardDeviation = 1.0): float
{
// From numerical recipes, page 320
return $mean - M_SQRT2 * $standardDeviation * GaussianDistribution::inverseErrorFunctionCumulativeTo(2 * $var);
return $mean - M_SQRT2 * $standardDeviation * GaussianDistribution::inverseErrorFunctionCumulativeTo(2.0 * $var);
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace DNW\Skills\Numerics;
class IdentityMatrix extends DiagonalMatrix
final class IdentityMatrix extends DiagonalMatrix
{
public function __construct(int $rows)
{

View File

@@ -8,7 +8,7 @@ use Exception;
class Matrix
{
public const ERROR_TOLERANCE = 0.0000000001;
public const float ERROR_TOLERANCE = 0.0000000001;
/**
* @param array<int,array<int,float>> $matrixRowData
@@ -79,7 +79,7 @@ class Matrix
}
}
public function getValue(int $row, int $col): float|int
public function getValue(int $row, int $col): float
{
$this->checkRowCol($row, $col);
return $this->matrixRowData[$row][$col];
@@ -212,7 +212,7 @@ class Matrix
return self::scalarMultiply($determinantInverse, $adjugate);
}
public static function scalarMultiply(float|int $scalarValue, Matrix $matrix): Matrix
public static function scalarMultiply(float $scalarValue, Matrix $matrix): Matrix
{
$rows = $matrix->getRowCount();
$columns = $matrix->getColumnCount();
@@ -265,7 +265,7 @@ class Matrix
for ($currentRow = 0; $currentRow < $resultRows; ++$currentRow) {
for ($currentColumn = 0; $currentColumn < $resultColumns; ++$currentColumn) {
$productValue = 0;
$productValue = 0.0;
for ($vectorIndex = 0; $vectorIndex < $left->getColumnCount(); ++$vectorIndex) {
$leftValue = $left->getValue($currentRow, $vectorIndex);

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace DNW\Skills\Numerics;
class SquareMatrix extends Matrix
final class SquareMatrix extends Matrix
{
public function __construct(float|int ...$allValues)
{

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace DNW\Skills\Numerics;
class Vector extends Matrix
final class Vector extends Matrix
{
/**
* @param float[] $vectorValues

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace DNW\Skills;
class PartialPlay
final class PartialPlay
{
public static function getPartialPlayPercentage(Player $player): float
{

View File

@@ -7,25 +7,31 @@ namespace DNW\Skills;
/**
* Represents a player who has a Rating.
*/
class Player implements ISupportPartialPlay, ISupportPartialUpdate
final readonly class Player implements ISupportPartialPlay, ISupportPartialUpdate
{
private const DEFAULT_PARTIAL_PLAY_PERCENTAGE = 1.0; // = 100% play time
private const float DEFAULT_PARTIAL_PLAY_PERCENTAGE = 1.0; // = 100% play time
private const DEFAULT_PARTIAL_UPDATE_PERCENTAGE = 1.0;
private const float DEFAULT_PARTIAL_UPDATE_PERCENTAGE = 1.0;
private readonly float $PartialPlayPct;
/**
* @var float The weight percentage to give this player when calculating a new rank.
*/
private float $PartialPlayPct;
private readonly float $PartialUpdatePct;
/**
* @var float Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update.
*/
private float $PartialUpdatePct;
/**
* Constructs a player.
*
* @param string|int $Id The identifier for the player, such as a name.
* @param string|int $Id The identifier for the player, such as a name.
* @param float $partialPlayPct The weight percentage to give this player when calculating a new rank.
* @param float $partialUpdatePct Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update.
*/
public function __construct(
private readonly mixed $Id,
private mixed $Id,
float $partialPlayPct = self::DEFAULT_PARTIAL_PLAY_PERCENTAGE,
float $partialUpdatePct = self::DEFAULT_PARTIAL_UPDATE_PERCENTAGE
)
@@ -47,6 +53,7 @@ class Player implements ISupportPartialPlay, ISupportPartialUpdate
/**
* Indicates the percent of the time the player should be weighted where 0.0 indicates the player didn't play and 1.0 indicates the player played 100% of the time.
*/
#[\Override]
public function getPartialPlayPercentage(): float
{
return $this->PartialPlayPct;
@@ -55,6 +62,7 @@ class Player implements ISupportPartialPlay, ISupportPartialUpdate
/**
* Indicated how much of a skill update a player should receive where 0.0 represents no update and 1.0 represents 100% of the update.
*/
#[\Override]
public function getPartialUpdatePercentage(): float
{
return $this->PartialUpdatePct;

View File

@@ -6,6 +6,6 @@ namespace DNW\Skills;
use DNW\Skills\Numerics\Range;
class PlayersRange extends Range
final class PlayersRange extends Range
{
}

View File

@@ -7,7 +7,7 @@ namespace DNW\Skills;
/**
* Helper class to sort ranks in non-decreasing order.
*/
class RankSorter
final class RankSorter
{
/**
* Performs an in-place sort of the items in according to the ranks in non-decreasing order.

View File

@@ -9,18 +9,18 @@ use DNW\Skills\Numerics\GaussianDistribution;
/**
* Container for a player's rating.
*/
class Rating
final readonly class Rating
{
private const CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER = 3;
private const float CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER = 3;
/**
* Constructs a rating.
*
* @param float $mean The statistical mean value of the rating (also known as mu).
* @param float $standardDeviation The standard deviation of the rating (also known as s).
* @param float|int $conservativeStandardDeviationMultiplier optional The number of standardDeviations to subtract from the mean to achieve a conservative rating.
* @param float $conservativeStandardDeviationMultiplier optional The number of standardDeviations to subtract from the mean to achieve a conservative rating.
*/
public function __construct(private readonly float $mean, private readonly float $standardDeviation, private readonly float|int $conservativeStandardDeviationMultiplier = self::CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER)
public function __construct(private float $mean, private float $standardDeviation, private float $conservativeStandardDeviationMultiplier = self::CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER)
{
}

View File

@@ -6,6 +6,9 @@ namespace DNW\Skills;
class RatingContainer
{
/**
* Link Player to a Rating using a hash map.
*/
private readonly HashMap $playerToRating;
public function __construct()

View File

@@ -11,11 +11,11 @@ use Exception;
*/
abstract class SkillCalculator
{
public const NONE = 0x00;
public const int NONE = 0x00;
public const PARTIAL_PLAY = 0x01;
public const int PARTIAL_PLAY = 0x01;
public const PARTIAL_UPDATE = 0x02;
public const int PARTIAL_UPDATE = 0x02;
protected function __construct(
private readonly int $supportedOptions,

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace DNW\Skills;
class Team extends RatingContainer
final class Team extends RatingContainer
{
public function __construct(Player $player = NULL, Rating $rating = NULL)
public function __construct(?Player $player = NULL, ?Rating $rating = NULL)
{
parent::__construct();
if (! $player instanceof Player) {

View File

@@ -6,6 +6,6 @@ namespace DNW\Skills;
use DNW\Skills\Numerics\Range;
class TeamsRange extends Range
final class TeamsRange extends Range
{
}

View File

@@ -18,6 +18,6 @@ final class DrawMargin
//
// margin = inversecdf((draw probability + 1)/2) * sqrt(n1+n2) * beta
// n1 and n2 are the number of players on each team
return GaussianDistribution::inverseCumulativeTo(.5 * ($drawProbability + 1), 0, 1) * M_SQRT2 * $beta;
return GaussianDistribution::inverseCumulativeTo(0.5 * ($drawProbability + 1.0), 0.0, 1.0) * M_SQRT2 * $beta;
}
}

View File

@@ -22,7 +22,7 @@ use DNW\Skills\Rating;
/**
* Calculates TrueSkill using a full factor graph.
*/
class FactorGraphTrueSkillCalculator extends SkillCalculator
final class FactorGraphTrueSkillCalculator extends SkillCalculator
{
public function __construct()
{
@@ -32,6 +32,7 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
/**
* {@inheritdoc}
*/
#[\Override]
public function calculateNewRatings(
GameInfo $gameInfo,
array $teams,
@@ -54,6 +55,7 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
/**
* {@inheritdoc}
*/
#[\Override]
public function calculateMatchQuality(GameInfo $gameInfo, array $teams): float
{
// We need to create the A matrix which is the player team assigments.
@@ -190,7 +192,7 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
$nextTeam = $teamAssignmentsList[$i + 1];
foreach ($nextTeam->getAllPlayers() as $nextTeamPlayer) {
// Add a -1 * playing time to represent the difference
$playerAssignments[$currentColumn][] = -1 * PartialPlay::getPartialPlayPercentage($nextTeamPlayer);
$playerAssignments[$currentColumn][] = -1.0 * PartialPlay::getPartialPlayPercentage($nextTeamPlayer);
--$rowsRemaining;
}

View File

@@ -14,6 +14,7 @@ abstract class GaussianFactor extends Factor
/**
* Sends the factor-graph message with and returns the log-normalization constant.
*/
#[\Override]
protected function sendMessageVariable(Message $message, Variable $variable): float|int
{
$marginal = $variable->getValue();
@@ -24,6 +25,7 @@ abstract class GaussianFactor extends Factor
return $logZ;
}
#[\Override]
public function createVariableToMessageBinding(Variable $variable): Message
{
$newDistribution = GaussianDistribution::fromPrecisionMean(0, 0);

View File

@@ -14,7 +14,7 @@ use DNW\Skills\TrueSkill\TruncatedGaussianCorrectionFunctions;
*
* See the accompanying math paper for more details.
*/
class GaussianGreaterThanFactor extends GaussianFactor
final class GaussianGreaterThanFactor extends GaussianFactor
{
public function __construct(private readonly float $epsilon, Variable $variable)
{
@@ -22,6 +22,7 @@ class GaussianGreaterThanFactor extends GaussianFactor
$this->createVariableToMessageBinding($variable);
}
#[\Override]
public function getLogNormalization(): float
{
$vars = $this->getVariables();
@@ -42,6 +43,7 @@ class GaussianGreaterThanFactor extends GaussianFactor
);
}
#[\Override]
protected function updateMessageVariable(Message $message, Variable $variable): float
{
$oldMarginal = clone $variable->getValue();

View File

@@ -15,7 +15,7 @@ use Exception;
*
* See the accompanying math paper for more details.
*/
class GaussianLikelihoodFactor extends GaussianFactor
final class GaussianLikelihoodFactor extends GaussianFactor
{
private readonly float $precision;
@@ -28,10 +28,11 @@ class GaussianLikelihoodFactor extends GaussianFactor
$this->createVariableToMessageBinding($variable2);
}
#[\Override]
public function getLogNormalization(): float
{
/**
* @var KeyedVariable[]|mixed $vars
* @var KeyedVariable[] $vars
*/
$vars = $this->getVariables();
/**
@@ -73,6 +74,7 @@ class GaussianLikelihoodFactor extends GaussianFactor
return GaussianDistribution::subtract($newMarginal, $marginal1);
}
#[\Override]
public function updateMessageIndex(int $messageIndex): float
{
$messages = $this->getMessages();

View File

@@ -13,7 +13,7 @@ use DNW\Skills\Numerics\GaussianDistribution;
*
* See the accompanying math paper for more details.
*/
class GaussianPriorFactor extends GaussianFactor
final class GaussianPriorFactor extends GaussianFactor
{
private readonly GaussianDistribution $newMessage;
@@ -29,6 +29,7 @@ class GaussianPriorFactor extends GaussianFactor
$this->createVariableToMessageBindingWithMessage($variable, $newMessage);
}
#[\Override]
protected function updateMessageVariable(Message $message, Variable $variable): float
{
$oldMarginal = clone $variable->getValue();

View File

@@ -16,7 +16,7 @@ use DNW\Skills\Numerics\GaussianDistribution;
*
* See the accompanying math paper for more details.
*/
class GaussianWeightedSumFactor extends GaussianFactor
final class GaussianWeightedSumFactor extends GaussianFactor
{
/**
* @var array<int[]> $varIndexOrdersForWeights
@@ -72,12 +72,12 @@ class GaussianWeightedSumFactor extends GaussianFactor
$weightsLength = $variableWeightsLength + 1;
for ($weightsIndex = 1; $weightsIndex < $weightsLength; ++$weightsIndex) {
$currentWeights = \array_fill(0, $variableWeightsLength, 0);
$currentWeights = \array_fill(0, $variableWeightsLength, 0.0);
$variableIndices = \array_fill(0, $variableWeightsLength + 1, 0);
$variableIndices = \array_fill(0, $variableWeightsLength + 1, 0.0);
$variableIndices[0] = $weightsIndex;
$currentWeightsSquared = \array_fill(0, $variableWeightsLength, 0);
$currentWeightsSquared = \array_fill(0, $variableWeightsLength, 0.0);
// keep a single variable to keep track of where we are in the array.
// This is helpful since we skip over one of the spots
@@ -90,9 +90,9 @@ class GaussianWeightedSumFactor extends GaussianFactor
$currentWeight = (-$variableWeights[$currentWeightSourceIndex] / $variableWeights[$weightsIndex - 1]);
if ($variableWeights[$weightsIndex - 1] == 0) {
if ($variableWeights[$weightsIndex - 1] == 0.0) {
// HACK: Getting around division by zero
$currentWeight = 0;
$currentWeight = 0.0;
}
$currentWeights[$currentDestinationWeightIndex] = $currentWeight;
@@ -112,7 +112,7 @@ class GaussianWeightedSumFactor extends GaussianFactor
$currentWeights[$currentDestinationWeightIndex] = $finalWeight;
$currentWeightsSquared[$currentDestinationWeightIndex] = BasicMath::square($finalWeight);
$variableIndices[count($variableWeights)] = 0;
$variableIndices[count($variableWeights)] = 0.0;
$this->varIndexOrdersForWeights[] = $variableIndices;
$this->weights[$weightsIndex] = $currentWeights;
@@ -127,6 +127,7 @@ class GaussianWeightedSumFactor extends GaussianFactor
}
}
#[\Override]
public function getLogNormalization(): float
{
$vars = $this->getVariables();
@@ -197,6 +198,7 @@ class GaussianWeightedSumFactor extends GaussianFactor
return $finalDiff;
}
#[\Override]
public function updateMessageIndex(int $messageIndex): float
{
$allMessages = $this->getMessages();

View File

@@ -14,7 +14,7 @@ use DNW\Skills\TrueSkill\TruncatedGaussianCorrectionFunctions;
*
* See the accompanying math paper for more details.
*/
class GaussianWithinFactor extends GaussianFactor
final class GaussianWithinFactor extends GaussianFactor
{
public function __construct(private readonly float $epsilon, Variable $variable)
{
@@ -23,6 +23,7 @@ class GaussianWithinFactor extends GaussianFactor
$this->createVariableToMessageBinding($variable);
}
#[\Override]
public function getLogNormalization(): float
{
/**
@@ -46,6 +47,7 @@ class GaussianWithinFactor extends GaussianFactor
return -GaussianDistribution::logProductNormalization($messageFromVariable, $message) + log($z);
}
#[\Override]
protected function updateMessageVariable(Message $message, Variable $variable): float
{
$oldMarginal = clone $variable->getValue();

View File

@@ -11,7 +11,7 @@ use DNW\Skills\TrueSkill\TrueSkillFactorGraph;
use Exception;
// The whole purpose of this is to do a loop on the bottom
class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
final class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
{
public function __construct(
TrueSkillFactorGraph $parentGraph,
@@ -22,6 +22,7 @@ class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
parent::__construct($parentGraph);
}
#[\Override]
public function getLocalFactors(): array
{
return array_merge(
@@ -30,6 +31,7 @@ class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
);
}
#[\Override]
public function buildLayer(): void
{
$inputVariablesGroups = $this->getInputVariablesGroups();
@@ -41,7 +43,8 @@ class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
$this->teamDifferencesComparisonLayer->buildLayer();
}
public function createPriorSchedule(): ?ScheduleSequence
#[\Override]
public function createPriorSchedule(): ScheduleSequence
{
switch (count($this->getInputVariablesGroups())) {
case 0:

View File

@@ -11,8 +11,9 @@ use DNW\Skills\TrueSkill\Factors\GaussianWeightedSumFactor;
use DNW\Skills\FactorGraphs\Variable;
use DNW\Skills\FactorGraphs\KeyedVariable;
class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLayer
final class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLayer
{
#[\Override]
public function buildLayer(): void
{
$inputVariablesGroups = $this->getInputVariablesGroups();
@@ -32,7 +33,8 @@ class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLaye
}
}
public function createPriorSchedule(): ?ScheduleSequence
#[\Override]
public function createPriorSchedule(): ScheduleSequence
{
$localFactors = $this->getLocalFactors();
@@ -49,7 +51,7 @@ class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLaye
/**
* @param KeyedVariable[] $teamMembers
*/
protected function createPlayerToTeamSumFactor(array $teamMembers, Variable $sumVariable): GaussianWeightedSumFactor
private function createPlayerToTeamSumFactor(array $teamMembers, Variable $sumVariable): GaussianWeightedSumFactor
{
$weights = array_map(
static function ($v): float {
@@ -66,7 +68,8 @@ class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLaye
);
}
public function createPosteriorSchedule(): ?ScheduleSequence
#[\Override]
public function createPosteriorSchedule(): ScheduleSequence
{
$allFactors = [];
$localFactors = $this->getLocalFactors();

View File

@@ -17,7 +17,7 @@ use DNW\Skills\FactorGraphs\ScheduleSequence;
// We intentionally have no Posterior schedule since the only purpose here is to
// start the process.
class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
final class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
{
/**
* @param Team[] $teams
@@ -27,6 +27,7 @@ class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
parent::__construct($parentGraph);
}
#[\Override]
public function buildLayer(): void
{
$teams = $this->teams;
@@ -49,7 +50,8 @@ class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
}
}
public function createPriorSchedule(): ?ScheduleSequence
#[\Override]
public function createPriorSchedule(): ScheduleSequence
{
$localFactors = $this->getLocalFactors();

View File

@@ -11,8 +11,9 @@ use DNW\Skills\Numerics\BasicMath;
use DNW\Skills\TrueSkill\Factors\GaussianLikelihoodFactor;
use DNW\Skills\FactorGraphs\ScheduleSequence;
class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
final class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
{
#[\Override]
public function buildLayer(): void
{
$inputVarGroups = $this->getInputVariablesGroups();
@@ -51,7 +52,8 @@ class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
return $this->getParentFactorGraph()->getVariableFactory()->createKeyedVariable($key);
}
public function createPriorSchedule(): ?ScheduleSequence
#[\Override]
public function createPriorSchedule(): ScheduleSequence
{
$localFactors = $this->getLocalFactors();
@@ -65,7 +67,8 @@ class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
);
}
public function createPosteriorSchedule(): ?ScheduleSequence
#[\Override]
public function createPosteriorSchedule(): ScheduleSequence
{
$localFactors = $this->getLocalFactors();

View File

@@ -9,7 +9,7 @@ use DNW\Skills\TrueSkill\Factors\GaussianGreaterThanFactor;
use DNW\Skills\TrueSkill\Factors\GaussianWithinFactor;
use DNW\Skills\TrueSkill\TrueSkillFactorGraph;
class TeamDifferencesComparisonLayer extends TrueSkillFactorGraphLayer
final class TeamDifferencesComparisonLayer extends TrueSkillFactorGraphLayer
{
private readonly float $epsilon;
@@ -23,6 +23,7 @@ class TeamDifferencesComparisonLayer extends TrueSkillFactorGraphLayer
$this->epsilon = DrawMargin::getDrawMarginFromDrawProbability($gameInfo->getDrawProbability(), $gameInfo->getBeta());
}
#[\Override]
public function buildLayer(): void
{
$inputVarGroups = $this->getInputVariablesGroups();

View File

@@ -7,8 +7,9 @@ namespace DNW\Skills\TrueSkill\Layers;
use DNW\Skills\FactorGraphs\Variable;
use DNW\Skills\TrueSkill\Factors\GaussianWeightedSumFactor;
class TeamPerformancesToTeamPerformanceDifferencesLayer extends TrueSkillFactorGraphLayer
final class TeamPerformancesToTeamPerformanceDifferencesLayer extends TrueSkillFactorGraphLayer
{
#[\Override]
public function buildLayer(): void
{
$inputVariablesGroups = $this->getInputVariablesGroups();
@@ -28,11 +29,7 @@ class TeamPerformancesToTeamPerformanceDifferencesLayer extends TrueSkillFactorG
}
}
private function createTeamPerformanceToDifferenceFactor(
Variable $strongerTeam,
Variable $weakerTeam,
Variable $output
): GaussianWeightedSumFactor
private function createTeamPerformanceToDifferenceFactor(Variable $strongerTeam, Variable $weakerTeam, Variable $output): GaussianWeightedSumFactor
{
$teams = [$strongerTeam, $weakerTeam];
$weights = [1.0, -1.0];

View File

@@ -9,8 +9,4 @@ use DNW\Skills\TrueSkill\TrueSkillFactorGraph;
abstract class TrueSkillFactorGraphLayer extends FactorGraphLayer
{
public function __construct(TrueSkillFactorGraph $parentGraph)
{
parent::__construct($parentGraph);
}
}

View File

@@ -23,7 +23,7 @@ use DNW\Skills\TrueSkill\Layers\PlayerSkillsToPerformancesLayer;
use DNW\Skills\TrueSkill\Layers\TeamDifferencesComparisonLayer;
use DNW\Skills\TrueSkill\Layers\TeamPerformancesToTeamPerformanceDifferencesLayer;
class TrueSkillFactorGraph extends FactorGraph
final class TrueSkillFactorGraph extends FactorGraph
{
/**
* @var FactorGraphLayer[] $layers
@@ -33,6 +33,8 @@ class TrueSkillFactorGraph extends FactorGraph
private readonly PlayerPriorValuesToSkillsLayer $priorLayer;
/**
* Constructor
*
* @param GameInfo $gameInfo Parameters for the game.
* @param Team[] $teams A mapping of team players and their ratings.
* @param int[] $teamRanks The ranks of the teams where 1 is first place. For a tie, repeat the number (e.g. 1, 2, 2).

View File

@@ -6,7 +6,7 @@ namespace DNW\Skills\TrueSkill;
use DNW\Skills\Numerics\GaussianDistribution;
class TruncatedGaussianCorrectionFunctions
final class TruncatedGaussianCorrectionFunctions
{
// These functions from the bottom of page 4 of the TrueSkill paper.

View File

@@ -21,7 +21,7 @@ use DNW\Skills\TeamsRange;
* When you only have two players, a lot of the math simplifies. The main purpose of this class
* is to show the bare minimum of what a TrueSkill implementation should have.
*/
class TwoPlayerTrueSkillCalculator extends SkillCalculator
final class TwoPlayerTrueSkillCalculator extends SkillCalculator
{
public function __construct()
{
@@ -31,11 +31,8 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
/**
* {@inheritdoc}
*/
public function calculateNewRatings(
GameInfo $gameInfo,
array $teams,
array $teamRanks
): RatingContainer
#[\Override]
public function calculateNewRatings(GameInfo $gameInfo, array $teams, array $teamRanks): RatingContainer
{
// Basic argument checking
$this->validateTeamCountAndPlayersCountPerTeam($teams);
@@ -94,7 +91,7 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
+
BasicMath::square($opponentRating->getStandardDeviation())
+
2 * BasicMath::square($gameInfo->getBeta())
2.0 * BasicMath::square($gameInfo->getBeta())
);
$winningMean = $selfRating->getMean();
@@ -117,11 +114,11 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
// non-draw case
$v = TruncatedGaussianCorrectionFunctions::vExceedsMarginScaled($meanDelta, $drawMargin, $c);
$w = TruncatedGaussianCorrectionFunctions::wExceedsMarginScaled($meanDelta, $drawMargin, $c);
$rankMultiplier = $comparison->value;
$rankMultiplier = (float)$comparison->value;
} else {
$v = TruncatedGaussianCorrectionFunctions::vWithinMarginScaled($meanDelta, $drawMargin, $c);
$w = TruncatedGaussianCorrectionFunctions::wWithinMarginScaled($meanDelta, $drawMargin, $c);
$rankMultiplier = 1;
$rankMultiplier = 1.0;
}
$meanMultiplier = (BasicMath::square($selfRating->getStandardDeviation()) + BasicMath::square($gameInfo->getDynamicsFactor())) / $c;
@@ -130,7 +127,7 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
$stdDevMultiplier = $varianceWithDynamics / BasicMath::square($c);
$newMean = $selfRating->getMean() + ($rankMultiplier * $meanMultiplier * $v);
$newStdDev = sqrt($varianceWithDynamics * (1 - $w * $stdDevMultiplier));
$newStdDev = sqrt($varianceWithDynamics * (1.0 - $w * $stdDevMultiplier));
return new Rating($newMean, $newStdDev);
}
@@ -138,6 +135,7 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
/**
* {@inheritdoc}
*/
#[\Override]
public function calculateMatchQuality(GameInfo $gameInfo, array $teams): float
{
$this->validateTeamCountAndPlayersCountPerTeam($teams);
@@ -158,16 +156,16 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
// This is the square root part of the equation:
$sqrtPart = sqrt(
(2 * $betaSquared)
(2.0 * $betaSquared)
/
(2 * $betaSquared + $player1SigmaSquared + $player2SigmaSquared)
(2.0 * $betaSquared + $player1SigmaSquared + $player2SigmaSquared)
);
// This is the exponent part of the equation:
$expPart = exp(
(-1 * BasicMath::square($player1Rating->getMean() - $player2Rating->getMean()))
(-1.0 * BasicMath::square($player1Rating->getMean() - $player2Rating->getMean()))
/
(2 * (2 * $betaSquared + $player1SigmaSquared + $player2SigmaSquared))
(2.0 * (2.0 * $betaSquared + $player1SigmaSquared + $player2SigmaSquared))
);
return $sqrtPart * $expPart;

View File

@@ -21,7 +21,7 @@ use DNW\Skills\TeamsRange;
*
* When you only have two teams, the math is still simple: no factor graphs are used yet.
*/
class TwoTeamTrueSkillCalculator extends SkillCalculator
final class TwoTeamTrueSkillCalculator extends SkillCalculator
{
public function __construct()
{
@@ -31,6 +31,7 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
/**
* {@inheritdoc}
*/
#[\Override]
public function calculateNewRatings(GameInfo $gameInfo, array $teams, array $teamRanks): RatingContainer
{
$this->validateTeamCountAndPlayersCountPerTeam($teams);
@@ -63,13 +64,7 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
return $results;
}
private static function updatePlayerRatings(
GameInfo $gameInfo,
RatingContainer $newPlayerRatings,
Team $selfTeam,
Team $otherTeam,
PairwiseComparison $selfToOtherTeamComparison
): void
private static function updatePlayerRatings(GameInfo $gameInfo, RatingContainer $newPlayerRatings, Team $selfTeam, Team $otherTeam, PairwiseComparison $selfToOtherTeamComparison): void
{
$drawMargin = DrawMargin::getDrawMarginFromDrawProbability(
$gameInfo->getDrawProbability(),
@@ -93,7 +88,7 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
+
BasicMath::sum($otherTeam->getAllRatings(), $varianceGetter)
+
$totalPlayers * $betaSquared
(float)$totalPlayers * $betaSquared
);
$winningMean = $selfMeanSum;
@@ -116,12 +111,12 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
// non-draw case
$v = TruncatedGaussianCorrectionFunctions::vExceedsMarginScaled($meanDelta, $drawMargin, $c);
$w = TruncatedGaussianCorrectionFunctions::wExceedsMarginScaled($meanDelta, $drawMargin, $c);
$rankMultiplier = $selfToOtherTeamComparison->value;
$rankMultiplier = (float)$selfToOtherTeamComparison->value;
} else {
// assume draw
$v = TruncatedGaussianCorrectionFunctions::vWithinMarginScaled($meanDelta, $drawMargin, $c);
$w = TruncatedGaussianCorrectionFunctions::wWithinMarginScaled($meanDelta, $drawMargin, $c);
$rankMultiplier = 1;
$rankMultiplier = 1.0;
}
$selfTeamAllPlayers = $selfTeam->getAllPlayers();
@@ -136,7 +131,7 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
$newMean = $previousPlayerRating->getMean() + $playerMeanDelta;
$newStdDev = sqrt(
(BasicMath::square($previousPlayerRating->getStandardDeviation()) + $tauSquared) * (1 - $w * $stdDevMultiplier)
(BasicMath::square($previousPlayerRating->getStandardDeviation()) + $tauSquared) * (1.0 - $w * $stdDevMultiplier)
);
$newPlayerRatings->setRating($localSelfTeamCurPlayer, new Rating($newMean, $newStdDev));
@@ -146,6 +141,7 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
/**
* {@inheritdoc}
*/
#[\Override]
public function calculateMatchQuality(GameInfo $gameInfo, array $teams): float
{
$this->validateTeamCountAndPlayersCountPerTeam($teams);
@@ -157,7 +153,7 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
$team2Ratings = $teams[1]->getAllRatings();
$team2Count = count($team2Ratings);
$totalPlayers = $team1Count + $team2Count;
$totalPlayers = (float)($team1Count + $team2Count);
$betaSquared = BasicMath::square($gameInfo->getBeta());
@@ -182,9 +178,9 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
);
$expPart = exp(
(-1 * BasicMath::square($team1MeanSum - $team2MeanSum))
(-1.0 * BasicMath::square($team1MeanSum - $team2MeanSum))
/
(2 * ($totalPlayers * $betaSquared + $team1StdDevSquared + $team2StdDevSquared))
(2.0 * ($totalPlayers * $betaSquared + $team1StdDevSquared + $team2StdDevSquared))
);
return $expPart * $sqrtPart;

View File

@@ -12,7 +12,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(Variable::class)]
#[UsesClass(GaussianDistribution::class)]
class VariableTest extends TestCase
final class VariableTest extends TestCase
{
public function testGetterSetter(): void
{

View File

@@ -12,7 +12,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(GameInfo::class)]
#[UsesClass(Rating::class)]
class GameInfoTest extends TestCase
final class GameInfoTest extends TestCase
{
public function testMembers(): void
{

View File

@@ -10,7 +10,7 @@ use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(Guard::class)]
class GuardTest extends TestCase
final class GuardTest extends TestCase
{
public function testargumentIsValidIndexArgumentAbove(): void
{

View File

@@ -10,7 +10,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
use stdClass;
#[CoversClass(HashMap::class)]
class HashMapTest extends TestCase
final class HashMapTest extends TestCase
{
public function testHashmap(): void
{

View File

@@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(BasicMath::class)]
class BasicMathTest extends TestCase
final class BasicMathTest extends TestCase
{
public function testSquare(): void
{
@@ -23,7 +23,7 @@ class BasicMathTest extends TestCase
$arr = [1, 1, 1, 1];
$func_return = static fn(float $f): float => $f;
$func_double = static fn(float $f): float => $f * 2;
$func_double = static fn(float $f): float => $f * 2.0;
$this->assertEquals(4, BasicMath::sum($arr, $func_return));
$this->assertEquals(8, BasicMath::sum($arr, $func_double));
}

View File

@@ -12,9 +12,9 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(GaussianDistribution::class)]
#[UsesClass(BasicMath::class)]
class GaussianDistributionTest extends TestCase
final class GaussianDistributionTest extends TestCase
{
private const ERROR_TOLERANCE = 0.000001;
private const float ERROR_TOLERANCE = 0.000001;
public function testGetters(): void
{
@@ -24,7 +24,7 @@ class GaussianDistributionTest extends TestCase
$this->assertEquals(9, $gd->getVariance());
$this->assertEquals(3, $gd->getStandardDeviation());
$this->assertEquals(1 / 9, $gd->getPrecision());
$this->assertEquals(1 / 9 * 10, $gd->getPrecisionMean());
$this->assertEquals(1.0 / 9.0 * 10.0, $gd->getPrecisionMean());
$this->assertEqualsWithDelta(0.13298076013, $gd->getNormalizationConstant(), GaussianDistributionTest::ERROR_TOLERANCE);
}
@@ -57,7 +57,7 @@ class GaussianDistributionTest extends TestCase
$product2 = GaussianDistribution::multiply($m4s5, $m6s7);
$expectedMean = (4 * BasicMath::square(7) + 6 * BasicMath::square(5)) / (BasicMath::square(5) + BasicMath::square(7));
$expectedMean = (4.0 * BasicMath::square(7) + 6.0 * BasicMath::square(5)) / (BasicMath::square(5) + BasicMath::square(7));
$this->assertEqualsWithDelta($expectedMean, $product2->getMean(), GaussianDistributionTest::ERROR_TOLERANCE);
$expectedSigma = sqrt(((BasicMath::square(5) * BasicMath::square(7)) / (BasicMath::square(5) + BasicMath::square(7))));
@@ -74,7 +74,7 @@ class GaussianDistributionTest extends TestCase
$this->assertEqualsWithDelta(2.0, $productDividedByStandardNormal->getMean(), GaussianDistributionTest::ERROR_TOLERANCE);
$this->assertEqualsWithDelta(3.0, $productDividedByStandardNormal->getStandardDeviation(), GaussianDistributionTest::ERROR_TOLERANCE);
$product2 = new GaussianDistribution((4 * BasicMath::square(7) + 6 * BasicMath::square(5)) / (BasicMath::square(5) + BasicMath::square(7)), sqrt(((BasicMath::square(5) * BasicMath::square(7)) / (BasicMath::square(5) + BasicMath::square(7)))));
$product2 = new GaussianDistribution((4.0 * BasicMath::square(7) + 6.0 * BasicMath::square(5)) / (BasicMath::square(5) + BasicMath::square(7)), sqrt(((BasicMath::square(5) * BasicMath::square(7)) / (BasicMath::square(5) + BasicMath::square(7)))));
$m4s5 = new GaussianDistribution(4, 5);
$product2DividedByM4S5 = GaussianDistribution::divide($product2, $m4s5);
$this->assertEqualsWithDelta(6.0, $product2DividedByM4S5->getMean(), GaussianDistributionTest::ERROR_TOLERANCE);

View File

@@ -20,7 +20,7 @@ use Exception;
#[CoversClass(DiagonalMatrix::class)]
#[CoversClass(Vector::class)]
// phpcs:disable PSR2.Methods.FunctionCallSignature,Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma
class MatrixTest extends TestCase
final class MatrixTest extends TestCase
{
public function testEmptyMatrix(): void
{
@@ -295,7 +295,7 @@ class MatrixTest extends TestCase
1, 0, 6);
$cInverse = $c->getInverse();
$d = Matrix::scalarMultiply((1.0 / 22), new SquareMatrix(24, -12, -2,
$d = Matrix::scalarMultiply((1.0 / 22.0), new SquareMatrix(24, -12, -2,
5, 3, -5,
-4, 2, 4));

View File

@@ -10,7 +10,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
use Exception;
#[CoversClass(Range::class)]
class RangeTest extends TestCase
final class RangeTest extends TestCase
{
public function testConstructInvalidParam(): void
{

View File

@@ -14,7 +14,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(PartialPlay::class)]
#[UsesClass(Player::class)]
#[UsesClass(Guard::class)]
class PartialPlayTest extends TestCase
final class PartialPlayTest extends TestCase
{
public function testgetPartialPlayPercentage(): void
{

View File

@@ -12,7 +12,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(Player::class)]
#[UsesClass(Guard::class)]
class PlayerTest extends TestCase
final class PlayerTest extends TestCase
{
public function testPlayerObjectGetterSetter(): void
{

View File

@@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(RankSorter::class)]
class RankSorterTest extends TestCase
final class RankSorterTest extends TestCase
{
public function testSort(): void
{

View File

@@ -18,7 +18,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[UsesClass(Player::class)]
#[UsesClass(Rating::class)]
#[UsesClass(Guard::class)]
class RatingContainerTest extends TestCase
final class RatingContainerTest extends TestCase
{
public function testRatingContainer(): void
{

View File

@@ -14,7 +14,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(Rating::class)]
#[UsesClass(BasicMath::class)]
#[UsesClass(GaussianDistribution::class)]
class RatingTest extends TestCase
final class RatingTest extends TestCase
{
public function testGetRatingParameters(): void
{

View File

@@ -17,7 +17,7 @@ use PHPUnit\Framework\Attributes\RequiresPhpunit;
#[UsesClass(PlayersRange::class)]
#[UsesClass(TeamsRange::class)]
#[RequiresPhpunit('<12.0')]
class SkillCalculatorTest extends TestCase
final class SkillCalculatorTest extends TestCase
{
public function testisSupported(): void
{

View File

@@ -20,7 +20,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[UsesClass(Player::class)]
#[UsesClass(Rating::class)]
#[UsesClass(Guard::class)]
class TeamTest extends TestCase
final class TeamTest extends TestCase
{
public function testTeam(): void
{

View File

@@ -14,9 +14,9 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(DrawMargin::class)]
#[UsesClass(BasicMath::class)]
#[UsesClass(GaussianDistribution::class)]
class DrawMarginTest extends TestCase
final class DrawMarginTest extends TestCase
{
private const ERROR_TOLERANCE = 0.000001;
private const float ERROR_TOLERANCE = 0.000001;
public function testGetDrawMarginFromDrawProbability(): void
{

View File

@@ -10,16 +10,9 @@ use DNW\Skills\Player;
use DNW\Skills\Team;
use DNW\Skills\TrueSkill\FactorGraphTrueSkillCalculator;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(FactorGraphTrueSkillCalculator::class)]
#[UsesClass(\DNW\Skills\Numerics\Range::class)]
#[UsesClass(\DNW\Skills\PlayersRange::class)]
#[UsesClass(\DNW\Skills\SkillCalculator::class)]
#[UsesClass(\DNW\Skills\TeamsRange::class)]
class FactorGraphTrueSkillCalculatorTest extends TestCase
final class FactorGraphTrueSkillCalculatorTest extends TestCase
{
#[CoversNothing]
public function testMicrosoftResearchExample(): void
@@ -75,6 +68,7 @@ class FactorGraphTrueSkillCalculatorTest extends TestCase
TrueSkillCalculatorTests::testPartialPlayScenarios($this, $calculator);
}
#[CoversNothing]
public function testMethodisSupported(): void
{
$calculator = new FactorGraphTrueSkillCalculator();

View File

@@ -11,11 +11,11 @@ use DNW\Skills\SkillCalculator;
use DNW\Skills\Team;
use PHPUnit\Framework\TestCase;
class TrueSkillCalculatorTests
final class TrueSkillCalculatorTests
{
private const ERROR_TOLERANCE_TRUESKILL = 0.085;
private const float ERROR_TOLERANCE_TRUESKILL = 0.085;
private const ERROR_TOLERANCE_MATCH_QUALITY = 0.0005;
private const float ERROR_TOLERANCE_MATCH_QUALITY = 0.0005;
// These are the roll-up ones
@@ -811,15 +811,15 @@ class TrueSkillCalculatorTests
private static function sixteenTeamsOfOneNotDrawn(TestCase $testClass, SkillCalculator $calculator): void
{
$player1 = new Player(1);
$player2 = new Player(2);
$player3 = new Player(3);
$player4 = new Player(4);
$player5 = new Player(5);
$player6 = new Player(6);
$player7 = new Player(7);
$player8 = new Player(8);
$player9 = new Player(9);
$player1 = new Player(1);
$player2 = new Player(2);
$player3 = new Player(3);
$player4 = new Player(4);
$player5 = new Player(5);
$player6 = new Player(6);
$player7 = new Player(7);
$player8 = new Player(8);
$player9 = new Player(9);
$player10 = new Player(10);
$player11 = new Player(11);
$player12 = new Player(12);
@@ -830,15 +830,15 @@ class TrueSkillCalculatorTests
$gameInfo = new GameInfo();
$team1 = new Team($player1, $gameInfo->getDefaultRating());
$team2 = new Team($player2, $gameInfo->getDefaultRating());
$team3 = new Team($player3, $gameInfo->getDefaultRating());
$team4 = new Team($player4, $gameInfo->getDefaultRating());
$team5 = new Team($player5, $gameInfo->getDefaultRating());
$team6 = new Team($player6, $gameInfo->getDefaultRating());
$team7 = new Team($player7, $gameInfo->getDefaultRating());
$team8 = new Team($player8, $gameInfo->getDefaultRating());
$team9 = new Team($player9, $gameInfo->getDefaultRating());
$team1 = new Team($player1, $gameInfo->getDefaultRating());
$team2 = new Team($player2, $gameInfo->getDefaultRating());
$team3 = new Team($player3, $gameInfo->getDefaultRating());
$team4 = new Team($player4, $gameInfo->getDefaultRating());
$team5 = new Team($player5, $gameInfo->getDefaultRating());
$team6 = new Team($player6, $gameInfo->getDefaultRating());
$team7 = new Team($player7, $gameInfo->getDefaultRating());
$team8 = new Team($player8, $gameInfo->getDefaultRating());
$team9 = new Team($player9, $gameInfo->getDefaultRating());
$team10 = new Team($player10, $gameInfo->getDefaultRating());
$team11 = new Team($player11, $gameInfo->getDefaultRating());
$team12 = new Team($player12, $gameInfo->getDefaultRating());

View File

@@ -14,7 +14,7 @@ use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(TruncatedGaussianCorrectionFunctions::class)]
#[UsesClass(BasicMath::class)]
#[UsesClass(GaussianDistribution::class)]
class TruncatedGaussianCorrectionFunctionsTest extends TestCase
final class TruncatedGaussianCorrectionFunctionsTest extends TestCase
{
public function testvGreaterThan(): void
{

View File

@@ -6,11 +6,9 @@ namespace DNW\Skills\Tests\TrueSkill;
use DNW\Skills\TrueSkill\TwoPlayerTrueSkillCalculator;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversNothing;
#[CoversClass(TwoPlayerTrueSkillCalculator::class)]
class TwoPlayerTrueSkillCalculatorTest extends TestCase
final class TwoPlayerTrueSkillCalculatorTest extends TestCase
{
#[CoversNothing]
public function testTwoPlayerTrueSkillCalculator(): void

View File

@@ -6,11 +6,9 @@ namespace DNW\Skills\Tests\TrueSkill;
use DNW\Skills\TrueSkill\TwoTeamTrueSkillCalculator;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversNothing;
#[CoversClass(TwoTeamTrueSkillCalculator::class)]
class TwoTeamTrueSkillCalculatorTest extends TestCase
final class TwoTeamTrueSkillCalculatorTest extends TestCase
{
#[CoversNothing]
public function testTwoTeamTrueSkillCalculator(): void