Compare commits

...

50 Commits

Author SHA1 Message Date
2212d45f61 Package update and implemented new fixes for Rector findings 2025-01-13 08:57:13 +00:00
24665d6e57 PHPCBF added
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-09-13 07:26:48 +00:00
79f4495a77 Package update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-09-12 13:36:35 +00:00
9df860b08b SkillTest deprecated method
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-08-16 07:57:08 +00:00
1e381bfab7 Quality of life
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-07-31 07:27:13 +00:00
f6acee18e5 More tests more resistance to mutation testing.
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
2024-07-25 11:24:13 +00:00
1b7e26a6b5 Requirements update. (Psalm to dev but analysis tool only) 2024-07-23 10:04:02 +00:00
cf588f3fc2 PHPMD reintroduced. 2024-07-04 09:38:03 +00:00
5dc0c7058d Readding metrics to output 2024-07-01 09:31:35 +00:00
ed9013df34 Typical update 2024-06-18 12:50:00 +00:00
da5d782e40 Simplifying 2024-05-21 12:24:59 +00:00
de6a414d2b Types with newest rector version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-21 06:50:32 +00:00
6fbc2540ab Config update
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-17 06:17:37 +00:00
4b3a328726 Additional tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-15 12:41:34 +00:00
82a5505438 Codestandards 2024-05-15 07:13:01 +00:00
cbf03c5736 Additional tests. 2024-05-15 07:01:15 +00:00
65db7f64ea Teams::concat was unnessecary 2024-05-14 11:12:26 +00:00
5cf6acdfb1 More tests 2024-05-14 09:41:17 +00:00
46dcbed28b Strict code coverage.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-14 08:46:43 +00:00
c33f62af2f Additional tests
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-13 15:17:13 +00:00
c202330a77 Modern syntax for CI
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-06 09:25:27 +00:00
1574f1c878 Code coverage not performed on buildserver.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-05-06 09:19:41 +00:00
002f07003a Dependecy upgrade
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-04-30 07:23:28 +00:00
063a64a4f6 Less Stringable
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-03-19 16:24:19 +00:00
796bd993d0 Even less strings 2024-03-19 15:56:57 +00:00
bbff1fbbbc More stringable removal for performance. 2024-03-19 15:48:29 +00:00
ae5d2a8b73 String based "name" for Variable class removed for performance 2024-03-19 15:09:13 +00:00
0095829906 Stringable removed. 2024-03-19 14:38:55 +00:00
e5a96226ca More stringable 2024-03-19 14:10:19 +00:00
11fafc129a Slowly getting rid of stringable. 2024-03-19 14:10:11 +00:00
73ef2f45c8 Quicker hash 2024-03-19 12:57:56 +00:00
fd91e9b0c1 Minor performance boost 2024-03-19 12:49:14 +00:00
7b09658c25 Minor fixes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-03-11 08:55:07 +00:00
4c8e1fcb1b Update test coverage
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-03-04 07:59:16 +00:00
f4a86016a8 Unittesting 2024-03-01 12:56:55 +00:00
ec00087025 More pedantric types.
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-29 10:42:31 +00:00
03d26de045 Tiny type work 2024-02-29 10:16:18 +00:00
231173dbf2 All test parsing
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-27 08:08:48 +00:00
49953d1329 PRoper links
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-26 09:43:53 +00:00
3a14da5388 More docs 2024-02-26 09:36:05 +00:00
783a12e744 Bit of documentation effort
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-26 09:19:39 +00:00
edc80f9a43 Typo correction
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-22 13:26:41 +00:00
61af342cfa More rector rules
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-21 14:10:01 +00:00
c781540aa6 rector: instanceOf, earlyReturn, strictBooleans 2024-02-21 14:02:35 +00:00
0184d0432b rector: codingStyle 2024-02-21 13:48:37 +00:00
f3e5912ebb Rector: codeQuality 2024-02-21 13:40:20 +00:00
703372fda9 Rector: deadcode. 2024-02-21 13:29:09 +00:00
df5b48b2c4 More static types using rector. 2024-02-21 13:23:39 +00:00
660fbd1486 Rector standards slowly being applied. PHP Version first. 2024-02-20 14:21:44 +00:00
c72112c5aa Minor dependency upgrade
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-02-20 10:43:33 +00:00
91 changed files with 58801 additions and 1201 deletions

View File

@ -9,5 +9,4 @@ exclude:
- vendor
warning: true
memory-limit: -1
no-cache: true
log-junit: "output/lint.xml"

View File

@ -1,15 +1,17 @@
pipeline:
requirements:
when:
- event: [push, tag]
steps:
- name: requirements
image: composer
commands:
- composer install --no-dev
run:
- name: run
image: php:cli-bookworm
commands:
- php examples/3teams.php
- php examples/basic.php
dependencies:
- name: test
image: composer
commands:
- composer install
- vendor/bin/phpunit tests
- vendor/bin/phpunit tests --no-coverage

View File

@ -1,3 +1,7 @@
---
title: README
...
# PHP TrueSkill Implementation
This is a PHP port of the Moserware.Skills project that's available at
http://github.com/moserware/Skills
@ -7,9 +11,3 @@ For more details on how the algorithm works, see
http://www.moserware.com/2010/03/computing-your-skill.html
For details on how to use this project, see the accompanying example snippets with this project.
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 overtrue/phplint
composer install
composer all

View File

@ -10,8 +10,10 @@ use DNW\Skills\TrueSkill\FactorGraphTrueSkillCalculator;
use DNW\Skills\GameInfo;
use DNW\Skills\Player;
use DNW\Skills\Team;
use DNW\Skills\Teams;
/**
* Basic Benchmarks.
*/
class BasicBench
{
/**
@ -30,8 +32,8 @@ class BasicBench
$team1 = new Team($p1, $gameInfo->getDefaultRating());
$team2 = new Team($p2, $gameInfo->getDefaultRating());
for ($i = 0; $i < 10; $i++) {
$teams = Teams::concat($team1, $team2);
for ($i = 0; $i < 10; ++$i) {
$teams = [$team1, $team2];
$calculator = new TwoPlayerTrueSkillCalculator();
@ -61,8 +63,8 @@ class BasicBench
$team1 = new Team($p1, $gameInfo->getDefaultRating());
$team2 = new Team($p2, $gameInfo->getDefaultRating());
for ($i = 0; $i < 10; $i++) {
$teams = Teams::concat($team1, $team2);
for ($i = 0; $i < 10; ++$i) {
$teams = [$team1, $team2];
$calculator = new TwoTeamTrueSkillCalculator();
@ -92,8 +94,8 @@ class BasicBench
$team1 = new Team($p1, $gameInfo->getDefaultRating());
$team2 = new Team($p2, $gameInfo->getDefaultRating());
for ($i = 0; $i < 10; $i++) {
$teams = Teams::concat($team1, $team2);
for ($i = 0; $i < 10; ++$i) {
$teams = [$team1, $team2];
$calculator = new FactorGraphTrueSkillCalculator();
@ -125,8 +127,8 @@ class BasicBench
$team2 = new Team($p2, $gameInfo->getDefaultRating());
$team3 = new Team($p3, $gameInfo->getDefaultRating());
for ($i = 0; $i < 10; $i++) {
$teams = Teams::concat($team1, $team2, $team3);
for ($i = 0; $i < 10; ++$i) {
$teams = [$team1, $team2, $team3];
$calculator = new FactorGraphTrueSkillCalculator();

View File

@ -7,10 +7,12 @@
},
"require-dev": {
"phpstan/phpstan": "^1.0",
"vimeo/psalm": "^5.21.1",
"vimeo/psalm": "dev-master",
"phpmetrics/phpmetrics": "^3.0-dev",
"phpunit/phpunit": "^10.5",
"psalm/plugin-phpunit": "^0.18.4"
"phpunit/phpunit": "^11.2",
"psalm/plugin-phpunit": "^0.18.4",
"rector/rector": "^1.0",
"league/csv": "^9.0"
},
"autoload": {
"psr-4": {
@ -26,23 +28,32 @@
"test": "phpunit",
"document": "phpDocumentor",
"benchmark": "phpbench run --report=default --output=build-artifact",
"metrics": "vendor/bin/phpmetrics --config=phpmetrics.json",
"metrics": "vendor/bin/phpmetrics --config=phpmetrics.yml",
"lint": [
"phplint",
"phpcs"
"phpcs",
"phpcbf src/ tests/ benchmark/ examples/",
"phpmd src/,tests/,benchmark/,examples/ text phpmd.ruleset.xml"
],
"static": [
"analyze": [
"@analyze-phpstan",
"@analyze-psalm"
"@analyze-psalm",
"@analyze-rector"
],
"analyze-phpstan":"vendor/bin/phpstan analyze --error-format=raw",
"analyze-psalm": "vendor/bin/psalm --no-cache",
"analyze-psalm": "vendor/bin/psalm --no-cache --show-info=true",
"analyze-rector": "vendor/bin/rector --dry-run",
"html": [
"pandoc -s README.md -o output/README.html",
"pandoc -s docs/index.rst -o output/index.html"
],
"all": [
"@test",
"@document",
"@benchmark",
"@lint",
"@static"
"@analyze",
"@document",
"@metrics",
"@html"
]
}
}

1660
composer.lock generated

File diff suppressed because it is too large Load Diff

53
docs/index.rst Normal file
View File

@ -0,0 +1,53 @@
Documentation
=============
This is a PHP port of the Moserware.Skills project that's available at
http://github.com/moserware/Skills
For more details on how the algorithm works, see
http://www.moserware.com/2010/03/computing-your-skill.html
For details on how to use this project, see the accompanying example snippets with this project.
https://www.microsoft.com/en-us/research/project/trueskill-ranking-system/
https://github.com/moserware/PHPSkills
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).
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).
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.
Links
-----
* `Project README <README.html>`_
* `API Documentations <docs/>`_
* `CodeCoverage <coverage/>`_
* `Test report <test/index.html>`_
* `Mutation testing <mutation/infection.html>`_
* `Code metrics <metrics/index.html>`_
Standard Tools
--------------
* PHP8.3
* PlantUML
* GraphViz
* Pandoc
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

@ -1,6 +1,6 @@
<?php
require_once("vendor/autoload.php");
require_once(__DIR__ . "/../vendor/autoload.php");
use DNW\Skills\TrueSkill\FactorGraphTrueSkillCalculator;
use DNW\Skills\GameInfo;
@ -20,9 +20,9 @@ $team2 = new Team($p2, $gameInfo->getDefaultRating());
$team3 = new Team($p3, $gameInfo->getDefaultRating());
for($i = 0; $i < 5; $i++) {
echo "Iteration: $i\n";
$teams = Teams::concat($team1, $team2, $team3);
for ($i = 0; $i < 5; ++$i) {
echo "Iteration: " . $i . PHP_EOL;
$teams = [$team1, $team2, $team3];
$calculator = new FactorGraphTrueSkillCalculator();
@ -36,6 +36,3 @@ for($i = 0; $i < 5; $i++) {
echo "P2: " . $newRatings->getRating($p2)->getConservativeRating() . PHP_EOL;
echo "P3: " . $newRatings->getRating($p3)->getConservativeRating() . PHP_EOL;
}

View File

@ -1,6 +1,6 @@
<?php
require_once("vendor/autoload.php");
require_once(__DIR__ . "/../vendor/autoload.php");
use DNW\Skills\TrueSkill\TwoPlayerTrueSkillCalculator;
use DNW\Skills\GameInfo;
@ -19,9 +19,9 @@ $team1 = new Team($p1, $gameInfo->getDefaultRating());
$team2 = new Team($p2, $gameInfo->getDefaultRating());
for($i = 0; $i < 5; $i++) {
echo "Iteration: $i\n";
$teams = Teams::concat($team1, $team2);
for ($i = 0; $i < 5; ++$i) {
echo "Iteration: " . $i . PHP_EOL;
$teams = [$team1, $team2];
$calculator = new TwoPlayerTrueSkillCalculator();
@ -33,6 +33,3 @@ for($i = 0; $i < 5; $i++) {
echo "P1: " . $newRatings->getRating($p1)->getConservativeRating() . PHP_EOL;
echo "P2: " . $newRatings->getRating($p2)->getConservativeRating() . PHP_EOL;
}

90
examples/motogp/goat.php Normal file
View File

@ -0,0 +1,90 @@
<?php
require __DIR__ . "/../../vendor/autoload.php";
use League\Csv\Reader;
use League\Csv\Statement;
use DNW\Skills\TrueSkill\FactorGraphTrueSkillCalculator;
use DNW\Skills\GameInfo;
use DNW\Skills\Player;
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->setDelimiter(',');
$csv->setHeaderOffset(0);
//build a statement
$stmt = Statement::create()->where(static fn (array $record): bool => $record['category'] == "MotoGP" || $record['category'] == "500cc");
/**
* @var $riders Player[]
*/
$riders = [];
//query your records from the document
$records = $stmt->process($csv);
$gameInfo = new GameInfo();
$calculator = new FactorGraphTrueSkillCalculator();
$first_record = $records->first();
$year_race = $first_record['year'] . '_' . $first_record['sequence'] . '_' . $first_record['category'];
$race_rate = [];
foreach ($records as $record) {
if ($year_race !== $record['year'] . '_' . $record['sequence'] . '_' . $record['category']) {
//Calculate the old race
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, $pos);
//update ratings
$highest_rate = 0;
$highest_rider = "";
foreach ($riders as $rider) {
//echo $rider['P']->getId().": ". $newRatings->getRating($rider['P'])->getConservativeRating() . PHP_EOL;
$rider['T']->setRating($rider['P'], $newRatings->getRating($rider['P']));
if ($newRatings->getRating($rider['P'])->getConservativeRating() > $highest_rate) {
$highest_rate = $newRatings->getRating($rider['P'])->getConservativeRating();
$highest_rider = $rider['P']->getId();
}
}
echo sprintf('Highest rider: %s => %s', $highest_rider, $highest_rate) . PHP_EOL;
foreach ($global_riders as $r) {
$rate = $r['T']->getRating($r['P'])->getConservativeRating();
$race_rate[$year_race][$r['P']->getId()] = $rate;
if (! isset($top_rating[$r['P']->getId()]) || $top_rating[$r['P']->getId()] < $rate) {
$top_rating[$r['P']->getId()] = $rate;
}
}
//prepare for next race
$year_race = $record['year'] . '_' . $record['sequence'] . '_' . $record['category'];
$races[] = ['year' => $record['year'], 'race' => $record['sequence'], 'circuit' => $record['circuit_name']];
echo "New Race: " . $year_race . ' => ' . $record['circuit_name'] . PHP_EOL;
$riders = [];
$teams = [];
$pos = [];
}
//Is it a new rider?
if (! isset($global_riders[$record['rider']])) {
$global_riders[$record['rider']]['P'] = new Player($record['rider_name']);
$global_riders[$record['rider']]['T'] = new Team($global_riders[$record['rider']]['P'], $gameInfo->getDefaultRating());
//echo "New Rider: ". $record['rider'] . " => ".$global_riders[$record['rider']]['P']->getId().PHP_EOL;
}
$riders[] = $global_riders[$record['rider']];
$teams[] = $global_riders[$record['rider']]['T'];
//Position or DNF?
$pos[] = $record['position'] >= 1 ? $record['position'] : end($pos);
}
echo "All time top score" . PHP_EOL;
asort($top_rating);
foreach ($top_rating as $n => $r) {
echo sprintf('%s => %s', $n, $r) . PHP_EOL;
}

56397
examples/motogp/motogp.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/infection/infection/0.27.9/resources/schema.json",
"$schema": "https://raw.githubusercontent.com/infection/infection/0.29.6/resources/schema.json",
"source": {
"directories": [
"src"
"src/"
]
},
"logs": {

View File

@ -2,9 +2,8 @@
"runner.bootstrap": "vendor/autoload.php",
"runner.path": "benchmark/",
"runner.php_disable_ini": true,
"runner.retry_threshold": 5,
"runner.retry_threshold": 10,
"runner.iterations": 10,
"storage.xml_storage_path": "output/benchmarking/",
"report.outputs": {
"build-artifact": {
"renderer": "html",
@ -12,5 +11,4 @@
"title": "Benchmarking"
}
}
}

View File

@ -3,16 +3,18 @@
configVersion="3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://www.phpdoc.org"
xsi:noNamespaceSchemaLocation="https://docs.phpdoc.org/latest/phpdoc.xsd"
>
<title>backupscript</title>
<title>trueskill</title>
<paths>
<output>output/docs</output>
</paths>
<version number="latest">
<api>
<source dsn=".">
<source>
<path>src</path>
</source>
</api>
</version>
<!--setting name="graphs.enabled" value="true"/-->
</phpdocumentor>

9
phpmd.baseline.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0"?>
<phpmd-baseline>
<violation rule="PHPMD\Rule\Design\TooManyPublicMethods" file="src/Numerics/GaussianDistribution.php"/>
<violation rule="PHPMD\Rule\UnusedPrivateMethod" file="src/Numerics/GaussianDistribution.php" method="errorFunctionCumulativeTo"/>
<violation rule="PHPMD\Rule\UnusedPrivateMethod" file="src/Numerics/GaussianDistribution.php" method="inverseErrorFunctionCumulativeTo"/>
<violation rule="PHPMD\Rule\Design\WeightedMethodCount" file="src/Numerics/Matrix.php"/>
<violation rule="PHPMD\Rule\Design\CouplingBetweenObjects" file="src/TrueSkill/FactorGraphTrueSkillCalculator.php"/>
<violation rule="PHPMD\Rule\Design\CouplingBetweenObjects" file="src/TrueSkill/TrueSkillFactorGraph.php"/>
</phpmd-baseline>

49
phpmd.ruleset.xml Normal file
View File

@ -0,0 +1,49 @@
<?xml version="1.0"?>
<ruleset name="TrueSkill custom PHPMD rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0
http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="
http://pmd.sf.net/ruleset_xml_schema.xsd">
<description>
TrueSkill custom PHPMD rules
</description>
<!-- Import the entire unused code rule set -->
<rule ref="rulesets/cleancode.xml">
<exclude name="StaticAccess" />
<exclude name="ElseExpression" />
</rule>
<rule ref="rulesets/codesize.xml" >
<exclude name="TooManyMethods" />
<exclude name="TooManyPublicMethods" />
</rule>
<rule ref="rulesets/codesize.xml/TooManyMethods">
<priority>1</priority>
<properties>
<property name="ignorepattern" value="#^(set|get|test)|test$#i" />
</properties>
</rule>
<rule ref="rulesets/codesize.xml/TooManyPublicMethods">
<priority>1</priority>
<properties>
<property name="ignorepattern" value="#^(set|get|test)|test$#i" />
</properties>
</rule>
<!--rule ref="rulesets/controversial.xml" /-->
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/naming.xml" >
<exclude name="LongClassName" />
<exclude name="ShortClassName" />
<exclude name="ShortVariable" />
<exclude name="LongVariable" />
<exclude name="ShortMethodName" />
</rule>
<rule ref="rulesets/unusedcode.xml" />
<!-- Import entire naming rule set and exclude rules -->
</ruleset>

View File

@ -1,21 +0,0 @@
{
"composer": true,
"includes": [
"src"
],
"excludes": [
"tests"
],
"report": {
"html": "output/metrics/",
"json": "output/metrics/report.json"
},
"plugins": {
"git": {
"binary": "git"
},
"junit": {
"file": "output/test.xml"
}
}
}

18
phpmetrics.yml Normal file
View File

@ -0,0 +1,18 @@
---
composer: true
includes:
- src
excludes:
- benchmark
- tests
extensions:
- php
report:
html: "output/metrics/"
json: "output/metrics/report.json"
violations: "/tmp/violations.xml"
plugins:
git:
binary: git
junit:
file: "output/test.xml"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false" displayDetailsOnTestsThatTriggerWarnings="true">
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" backupGlobals="false" bootstrap="vendor/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" cacheDirectory=".phpunit.cache" backupStaticProperties="false" displayDetailsOnTestsThatTriggerWarnings="true" beStrictAboutCoverageMetadata="true" requireCoverageMetadata="true">
<testsuites>
<testsuite name="PHPSkills Test Suite">
<directory>./tests/</directory>

20
rector.php Normal file
View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\CodeQuality\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/benchmark',
__DIR__ . '/examples',
__DIR__ . '/src',
__DIR__ . '/tests',
])
// uncomment to reach your current PHP version
->withPhpSets()
->withPreparedSets(deadCode: true, codeQuality: true, codingStyle: true, typeDeclarations : true, privatization: true, naming: false, instanceOf: true, earlyReturn: true, strictBooleans: true)
->withSkip([
LocallyCalledStaticMethodToNonStaticRector::class
]);;

View File

@ -8,32 +8,29 @@ use DNW\Skills\Guard;
use DNW\Skills\HashMap;
use Exception;
abstract class Factor implements \Stringable
abstract class Factor
{
/**
* @var Message[] $messages
*/
private array $messages = [];
private HashMap $messageToVariableBinding;
private string $name;
private readonly HashMap $msgToVariableBinding;
/**
* @var Variable[] $variables
*/
private array $variables = [];
protected function __construct(string $name)
protected function __construct()
{
$this->name = 'Factor[' . $name . ']';
$this->messageToVariableBinding = new HashMap();
$this->msgToVariableBinding = new HashMap();
}
/**
* @return mixed The log-normalization constant of that factor
* @return float The log-normalization constant of that factor
*/
public function getLogNormalization()
public function getLogNormalization(): float
{
return 0;
}
@ -73,14 +70,14 @@ abstract class Factor implements \Stringable
{
Guard::argumentIsValidIndex($messageIndex, count($this->messages), 'messageIndex');
$message = $this->messages[$messageIndex];
$variable = $this->messageToVariableBinding->getValue($message);
$variable = $this->msgToVariableBinding->getValue($message);
return $this->updateMessageVariable($message, $variable);
}
protected function updateMessageVariable(Message $message, Variable $variable): float
{
throw new Exception();
throw new Exception("Must override updateMessageVariable(" . $message::class . ", " . $variable::class . ")");
}
/**
@ -88,7 +85,7 @@ abstract class Factor implements \Stringable
*/
public function resetMarginals(): void
{
$allValues = $this->messageToVariableBinding->getAllValues();
$allValues = $this->msgToVariableBinding->getAllValues();
foreach ($allValues as $currentVariable) {
$currentVariable->resetToPrior();
}
@ -104,7 +101,7 @@ abstract class Factor implements \Stringable
Guard::argumentIsValidIndex($messageIndex, count($this->messages), 'messageIndex');
$message = $this->messages[$messageIndex];
$variable = $this->messageToVariableBinding->getValue($message);
$variable = $this->msgToVariableBinding->getValue($message);
return $this->sendMessageVariable($message, $variable);
}
@ -115,15 +112,10 @@ abstract class Factor implements \Stringable
protected function createVariableToMessageBindingWithMessage(Variable $variable, Message $message): Message
{
$this->messageToVariableBinding->setValue($message, $variable);
$this->msgToVariableBinding->setValue($message, $variable);
$this->messages[] = $message;
$this->variables[] = $variable;
return $message;
}
public function __toString(): string
{
return $this->name;
}
}

View File

@ -4,10 +4,15 @@ declare(strict_types=1);
namespace DNW\Skills\FactorGraphs;
class FactorGraph
abstract class FactorGraph
{
private VariableFactory $variableFactory;
protected function __construct()
{
$this->variableFactory = new VariableFactory(static fn(): null => NULL);
}
public function getVariableFactory(): VariableFactory
{
return $this->variableFactory;

View File

@ -15,28 +15,27 @@ abstract class FactorGraphLayer
private array $localFactors = [];
/**
* @var array<int,array<int,object>>
* @var array<int,array<int,Variable>>
*/
private array $outputVariablesGroups = [];
private array $outputVarGroups = [];
/**
* @var array<int,array<int,object>>
* @var array<int,array<int,Variable>>
*/
private $inputVariablesGroups = [];
private array $inputVariablesGroups = [];
protected function __construct(private readonly TrueSkillFactorGraph $parentFactorGraph)
{
}
/**
* @return array<int,array<int,object>>
* @return array<int,array<int,Variable>>
*/
protected function getInputVariablesGroups(): array
{
return $this->inputVariablesGroups;
}
// HACK
public function getParentFactorGraph(): TrueSkillFactorGraph
{
return $this->parentFactorGraph;
@ -45,11 +44,11 @@ abstract class FactorGraphLayer
/**
* This reference is still needed
*
* @return array<int,array<int,object>>
* @return array<int,array<int,Variable>>
*/
public function &getOutputVariablesGroups(): array
{
return $this->outputVariablesGroups;
return $this->outputVarGroups;
}
/**
@ -61,7 +60,7 @@ abstract class FactorGraphLayer
}
/**
* @param array<int,array<int,object>> $value
* @param array<int,array<int,Variable>> $value
*/
public function setInputVariablesGroups(array $value): void
{
@ -71,9 +70,9 @@ abstract class FactorGraphLayer
/**
* @param Schedule[] $itemsToSequence
*/
protected function scheduleSequence(array $itemsToSequence, string $name): ScheduleSequence
protected function scheduleSequence(array $itemsToSequence): ScheduleSequence
{
return new ScheduleSequence($name, $itemsToSequence);
return new ScheduleSequence($itemsToSequence);
}
protected function addLayerFactor(Factor $factor): void

View File

@ -25,13 +25,13 @@ class FactorList
$listCount = count($this->list);
for ($i = 0; $i < $listCount; $i++) {
$f = $this->list[$i];
for ($i = 0; $i < $listCount; ++$i) {
$factor = $this->list[$i];
$numberOfMessages = $f->getNumberOfMessages();
$numberOfMessages = $factor->getNumberOfMessages();
for ($j = 0; $j < $numberOfMessages; $j++) {
$sumLogZ += $f->sendMessageIndex($j);
for ($j = 0; $j < $numberOfMessages; ++$j) {
$sumLogZ += $factor->sendMessageIndex($j);
}
}

View File

@ -6,9 +6,9 @@ namespace DNW\Skills\FactorGraphs;
class KeyedVariable extends Variable
{
public function __construct(private mixed $key, string $name, mixed $prior)
public function __construct(private readonly mixed $key, mixed $prior)
{
parent::__construct($name, $prior);
parent::__construct($prior);
}
public function getKey(): mixed

View File

@ -6,9 +6,9 @@ namespace DNW\Skills\FactorGraphs;
use DNW\Skills\Numerics\GaussianDistribution;
class Message implements \Stringable
class Message
{
public function __construct(private GaussianDistribution $value, private string $name)
public function __construct(private GaussianDistribution $value)
{
}
@ -21,9 +21,4 @@ class Message implements \Stringable
{
$this->value = $value;
}
public function __toString(): string
{
return $this->name;
}
}

View File

@ -4,16 +4,7 @@ declare(strict_types=1);
namespace DNW\Skills\FactorGraphs;
abstract class Schedule implements \Stringable
abstract class Schedule
{
protected function __construct(private string $name)
{
}
abstract public function visit(int $depth = -1, int $maxDepth = 0): float;
public function __toString(): string
{
return $this->name;
}
}

View File

@ -6,9 +6,8 @@ namespace DNW\Skills\FactorGraphs;
class ScheduleLoop extends Schedule
{
public function __construct(string $name, private readonly Schedule $scheduleToLoop, private float $maxDelta)
public function __construct(private readonly Schedule $scheduleToLoop, private readonly float $maxDelta)
{
parent::__construct($name);
}
public function visit(int $depth = -1, int $maxDepth = 0): float

View File

@ -9,9 +9,8 @@ class ScheduleSequence extends Schedule
/**
* @param Schedule[] $schedules
*/
public function __construct(string $name, private readonly array $schedules)
public function __construct(private readonly array $schedules)
{
parent::__construct($name);
}
public function visit(int $depth = -1, int $maxDepth = 0): float

View File

@ -6,15 +6,12 @@ namespace DNW\Skills\FactorGraphs;
class ScheduleStep extends Schedule
{
public function __construct(string $name, private readonly Factor $factor, private int $index)
public function __construct(private readonly Factor $factor, private readonly int $index)
{
parent::__construct($name);
}
public function visit(int $depth = -1, int $maxDepth = 0): float
{
$currentFactor = $this->factor;
return $currentFactor->updateMessageIndex($this->index);
return $this->factor->updateMessageIndex($this->index);
}
}

View File

@ -6,15 +6,12 @@ namespace DNW\Skills\FactorGraphs;
use DNW\Skills\Numerics\GaussianDistribution;
class Variable implements \Stringable
class Variable
{
private string $name;
private mixed $value;
public function __construct(string $name, private GaussianDistribution $prior)
public function __construct(private readonly GaussianDistribution $prior)
{
$this->name = 'Variable[' . $name . ']';
$this->resetToPrior();
}
@ -32,9 +29,4 @@ class Variable implements \Stringable
{
$this->value = $this->prior;
}
public function __toString(): string
{
return $this->name;
}
}

View File

@ -6,21 +6,21 @@ namespace DNW\Skills\FactorGraphs;
class VariableFactory
{
public function __construct(private \Closure $variablePriorInitializer)
public function __construct(private readonly \Closure $varPriorInitializer)
{
}
public function createBasicVariable(string $name): Variable
public function createBasicVariable(): Variable
{
$initializer = $this->variablePriorInitializer;
$initializer = $this->varPriorInitializer;
return new Variable($name, $initializer());
return new Variable($initializer());
}
public function createKeyedVariable(mixed $key, string $name): KeyedVariable
public function createKeyedVariable(mixed $key): KeyedVariable
{
$initializer = $this->variablePriorInitializer;
$initializer = $this->varPriorInitializer;
return new KeyedVariable($key, $name, $initializer());
return new KeyedVariable($key, $initializer());
}
}

View File

@ -9,22 +9,28 @@ namespace DNW\Skills;
*/
class GameInfo
{
private const DEFAULT_BETA = 4.1666666666666666666666666666667; // Default initial mean / 6
/**
* Default initial mean / 6
*/
private const DEFAULT_BETA = 4.1666666666666666666666666666667;
private const DEFAULT_DRAW_PROBABILITY = 0.10;
private const DEFAULT_DYNAMICS_FACTOR = 0.083333333333333333333333333333333; // Default initial mean / 300
/**
* Default initial mean / 300
*/
private const DEFAULT_DYNAMICS_FACTOR = 0.083333333333333333333333333333333;
private const DEFAULT_INITIAL_MEAN = 25.0;
private const DEFAULT_INITIAL_STANDARD_DEVIATION = 8.3333333333333333333333333333333;
public function __construct(
private float $initialMean = self::DEFAULT_INITIAL_MEAN,
private float $initialStandardDeviation = 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
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
)
{
}
@ -36,7 +42,7 @@ class GameInfo
public function getInitialStandardDeviation(): float
{
return $this->initialStandardDeviation;
return $this->initialStdDev;
}
public function getBeta(): float
@ -56,6 +62,6 @@ class GameInfo
public function getDefaultRating(): Rating
{
return new Rating($this->initialMean, $this->initialStandardDeviation);
return new Rating($this->initialMean, $this->initialStdDev);
}
}

View File

@ -13,13 +13,6 @@ use Exception;
*/
class Guard
{
public static function argumentNotNull(mixed $value, string $parameterName): void
{
if ($value == NULL) {
throw new Exception($parameterName . ' can not be null');
}
}
public static function argumentIsValidIndex(int $index, int $count, string $parameterName): void
{
if (($index < 0) || ($index >= $count)) {

View File

@ -19,16 +19,16 @@ class HashMap
*/
private array $hashToKey = [];
public function getValue(string|object $key): mixed
public function getValue(object $key): mixed
{
$hash = self::getHash($key);
$hash = spl_object_id($key);
return $this->hashToValue[$hash];
}
public function setValue(string|object $key, mixed $value): self
public function setValue(object $key, mixed $value): self
{
$hash = self::getHash($key);
$hash = spl_object_id($key);
$this->hashToKey[$hash] = $key;
$this->hashToValue[$hash] = $value;
@ -55,13 +55,4 @@ class HashMap
{
return count($this->hashToKey);
}
private static function getHash(string|object $key): string
{
if (is_object($key)) {
return spl_object_hash($key);
}
return $key;
}
}

View File

@ -13,15 +13,15 @@ namespace DNW\Skills\Numerics;
class BasicMath
{
/**
* Squares the input (x^2 = x * x)
* Squares the input (input^2 = input * input)
*
* @param $x Value to square (x)
* @param $input Value to square (input)
*
* @return float The squared value (x^2)
* @return float The squared value (input^2)
*/
public static function square(float $x): float
public static function square(float $input): float
{
return $x * $x;
return $input * $input;
}
/**

View File

@ -15,8 +15,8 @@ class DiagonalMatrix extends Matrix
parent::__construct($diagonalCount, $diagonalCount);
for ($currentRow = 0; $currentRow < $diagonalCount; $currentRow++) {
for ($currentCol = 0; $currentCol < $diagonalCount; $currentCol++) {
for ($currentRow = 0; $currentRow < $diagonalCount; ++$currentRow) {
for ($currentCol = 0; $currentCol < $diagonalCount; ++$currentCol) {
if ($currentRow === $currentCol) {
$this->setValue($currentRow, $currentCol, $diagonalValues[$currentRow]);
} else {

View File

@ -10,33 +10,51 @@ namespace DNW\Skills\Numerics;
* @author Jeff Moser <jeff@moserware.com>
* @copyright 2010 Jeff Moser
*/
class GaussianDistribution implements \Stringable
class GaussianDistribution
{
//sqrt(2*pi)
//from https://www.wolframalpha.com/input?i=sqrt%282*pi%29
private const DEFAULT_STANDARD_DEVIATION = 1.0;
private const DEFAULT_MEAN = 0.0;
/**
* Square Root 2π.
* Precalculated constant for performance reasons
* sqrt(2*pi)
*
* @link https://www.wolframalpha.com/input?i=sqrt%282*pi%29 Source of value
*/
private const M_SQRT_2_PI = 2.5066282746310005024157652848110452530069867406099383166299235763;
//log(sqrt(2*pi))
//From https://www.wolframalpha.com/input?i=log%28sqrt%282*pi%29%29
/**
* Log of Square Root 2π.
* Precalculated constant for performance reasons
* log(sqrt(2*pi))
*
* @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;
// precision and precisionMean are used because they make multiplying and dividing simpler
// (the the accompanying math paper for more details)
private float $precision;
// (see the accompanying math paper for more details)
private float $precision = 1.0;
private float $precisionMean;
private float $precisionMean = 0.0;
private float $variance;
private float $variance = 1.0;
public function __construct(private float $mean = 0.0, private float $standardDeviation = 1.0)
public function __construct(private float $mean = self::DEFAULT_MEAN, private float $standardDeviation = self::DEFAULT_STANDARD_DEVIATION)
{
$this->variance = BasicMath::square($standardDeviation);
if ($mean == self::DEFAULT_MEAN && $standardDeviation == self::DEFAULT_STANDARD_DEVIATION) {
//Use all the defaults
return;
}
$this->variance = BasicMath::square($standardDeviation);
if ($this->variance != 0) {
$this->precision = 1.0 / $this->variance;
$this->precisionMean = $this->precision * $this->mean;
} else {
$this->precision = \INF;
$this->precisionMean = $this->mean == 0 ? 0 : \INF;
}
}
@ -66,9 +84,13 @@ class GaussianDistribution implements \Stringable
return $this->precisionMean;
}
/**
* Great derivation of this is at
*
* @link http://www.astro.psu.edu/~mce/A451_2/A451/downloads/notes0.pdf
*/
public function getNormalizationConstant(): float
{
// Great derivation of this is at http://www.astro.psu.edu/~mce/A451_2/A451/downloads/notes0.pdf
return 1.0 / (self::M_SQRT_2_PI * $this->standardDeviation);
}
@ -91,14 +113,18 @@ class GaussianDistribution implements \Stringable
return $result;
}
// For details, see http://www.tina-vision.net/tina-knoppix/tina-memo/2003-003.pdf
// for multiplication, the precision mean ones are easier to write :)
/**
* For details, see http://www.tina-vision.net/tina-knoppix/tina-memo/2003-003.pdf
* for multiplication, the precision mean ones are easier to write :)
*/
public static function multiply(GaussianDistribution $left, GaussianDistribution $right): self
{
return GaussianDistribution::fromPrecisionMean($left->precisionMean + $right->precisionMean, $left->precision + $right->precision);
}
// Computes the absolute difference between two Gaussians
/**
* Computes the absolute difference between two Gaussians
*/
public static function absoluteDifference(GaussianDistribution $left, GaussianDistribution $right): float
{
return max(
@ -107,7 +133,9 @@ class GaussianDistribution implements \Stringable
);
}
// Computes the absolute difference between two Gaussians
/**
* Computes the absolute difference between two Gaussians
*/
public static function subtract(GaussianDistribution $left, GaussianDistribution $right): float
{
return GaussianDistribution::absoluteDifference($left, $right);
@ -146,7 +174,7 @@ class GaussianDistribution implements \Stringable
BasicMath::square($meanDifference) / (2 * $varianceDifference);
}
public static function at(float $x, float $mean = 0.0, float $standardDeviation = 1.0): float
public static function at(float $var, float $mean = 0.0, float $standardDeviation = 1.0): float
{
// See http://mathworld.wolfram.com/NormalDistribution.html
// 1 -(x-mean)^2 / (2*stdDev^2)
@ -154,22 +182,22 @@ class GaussianDistribution implements \Stringable
// stdDev * sqrt(2*pi)
$multiplier = 1.0 / ($standardDeviation * self::M_SQRT_2_PI);
$expPart = exp((-1.0 * BasicMath::square($x - $mean)) / (2 * BasicMath::square($standardDeviation)));
$expPart = exp((-1.0 * BasicMath::square($var - $mean)) / (2 * BasicMath::square($standardDeviation)));
return $multiplier * $expPart;
}
public static function cumulativeTo(float $x, float $mean = 0.0, float $standardDeviation = 1.0): float
public static function cumulativeTo(float $var): float
{
$result = GaussianDistribution::errorFunctionCumulativeTo(-M_SQRT1_2 * $x);
$result = GaussianDistribution::errorFunctionCumulativeTo(-M_SQRT1_2 * $var);
return 0.5 * $result;
}
private static function errorFunctionCumulativeTo(float $x): float
private static function errorFunctionCumulativeTo(float $var): float
{
// Derived from page 265 of Numerical Recipes 3rd Edition
$z = abs($x);
$z = abs($var);
$t = 2.0 / (2.0 + $z);
$ty = 4 * $t - 2;
@ -209,7 +237,7 @@ class GaussianDistribution implements \Stringable
$d = 0.0;
$dd = 0.0;
for ($j = $ncof - 1; $j > 0; $j--) {
for ($j = $ncof - 1; $j > 0; --$j) {
$tmp = $d;
$d = $ty * $d - $dd + $coefficients[$j];
$dd = $tmp;
@ -217,16 +245,16 @@ class GaussianDistribution implements \Stringable
$ans = $t * exp(-$z * $z + 0.5 * ($coefficients[0] + $ty * $d) - $dd);
return ($x >= 0.0) ? $ans : (2.0 - $ans);
return ($var >= 0.0) ? $ans : (2.0 - $ans);
}
private static function inverseErrorFunctionCumulativeTo(float $p): float
{
// From page 265 of numerical recipes
if ($p >= 2.0) {
return -100;
}
if ($p <= 0.0) {
return 100;
}
@ -235,7 +263,7 @@ class GaussianDistribution implements \Stringable
$t = sqrt(-2 * 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++) {
for ($j = 0; $j < 2; ++$j) {
$err = GaussianDistribution::errorFunctionCumulativeTo($x) - $pp;
$x += $err / (M_2_SQRTPI * exp(-BasicMath::square($x)) - $x * $err); // Halley
}
@ -243,14 +271,9 @@ class GaussianDistribution implements \Stringable
return ($p < 1.0) ? $x : -$x;
}
public static function inverseCumulativeTo(float $x, float $mean = 0.0, float $standardDeviation = 1.0): float
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 * $x);
}
public function __toString(): string
{
return sprintf('mean=%.4f standardDeviation=%.4f', $this->mean, $this->standardDeviation);
return $mean - M_SQRT2 * $standardDeviation * GaussianDistribution::inverseErrorFunctionCumulativeTo(2 * $var);
}
}

View File

@ -13,7 +13,7 @@ class Matrix
/**
* @param array<int,array<int,float>> $matrixRowData
*/
public function __construct(private int $rowCount = 0, private int $columnCount = 0, private array $matrixRowData = [])
public function __construct(private readonly int $rowCount = 0, private readonly int $columnCount = 0, private array $matrixRowData = [])
{
}
@ -25,10 +25,10 @@ class Matrix
$data = [];
$result = new Matrix($rows, $columns, $data);
for ($currentColumn = 0; $currentColumn < $columns; $currentColumn++) {
for ($currentColumn = 0; $currentColumn < $columns; ++$currentColumn) {
$currentColumnData = $columnValues[$currentColumn];
for ($currentRow = 0; $currentRow < $rows; $currentRow++) {
for ($currentRow = 0; $currentRow < $rows; ++$currentRow) {
$result->setValue($currentRow, $currentColumn, $currentColumnData[$currentRow]);
}
}
@ -41,8 +41,8 @@ class Matrix
$result = new Matrix($rows, $cols);
$currentIndex = 0;
for ($currentRow = 0; $currentRow < $rows; $currentRow++) {
for ($currentCol = 0; $currentCol < $cols; $currentCol++) {
for ($currentRow = 0; $currentRow < $rows; ++$currentRow) {
for ($currentCol = 0; $currentCol < $cols; ++$currentCol) {
$result->setValue($currentRow, $currentCol, $args[$currentIndex++]);
}
}
@ -60,13 +60,34 @@ class Matrix
return $this->columnCount;
}
private function checkRowCol(int $row, int $col): void
{
if ($row < 0) {
throw new Exception("Row negative");
}
if ($row >= $this->getRowCount()) {
throw new Exception("Row beyond range");
}
if ($col < 0) {
throw new Exception("Column negative");
}
if ($col >= $this->getColumnCount()) {
throw new Exception("Column beyond range");
}
}
public function getValue(int $row, int $col): float|int
{
$this->checkRowCol($row, $col);
return $this->matrixRowData[$row][$col];
}
public function setValue(int $row, int $col, float|int $value): void
{
$this->checkRowCol($row, $col);
$this->matrixRowData[$row][$col] = $value;
}
@ -76,10 +97,10 @@ class Matrix
$transposeMatrix = [];
$rowMatrixData = $this->matrixRowData;
for ($currentRowTransposeMatrix = 0; $currentRowTransposeMatrix < $this->columnCount; $currentRowTransposeMatrix++) {
for ($currentColumnTransposeMatrix = 0; $currentColumnTransposeMatrix < $this->rowCount; $currentColumnTransposeMatrix++) {
$transposeMatrix[$currentRowTransposeMatrix][$currentColumnTransposeMatrix] =
$rowMatrixData[$currentColumnTransposeMatrix][$currentRowTransposeMatrix];
for ($curRowTransposeMx = 0; $curRowTransposeMx < $this->columnCount; ++$curRowTransposeMx) {
for ($curColTransposeMx = 0; $curColTransposeMx < $this->rowCount; ++$curColTransposeMx) {
$transposeMatrix[$curRowTransposeMx][$curColTransposeMx] =
$rowMatrixData[$curColTransposeMx][$curRowTransposeMx];
}
}
@ -88,7 +109,7 @@ class Matrix
private function isSquare(): bool
{
return ($this->rowCount == $this->columnCount) && ($this->rowCount > 0);
return ($this->rowCount === $this->columnCount) && ($this->rowCount > 0);
}
public function getDeterminant(): float
@ -100,7 +121,7 @@ class Matrix
if ($this->rowCount == 1) {
// Really happy path :)
return $this->matrixRowData[0][0];
return $this->getValue(0, 0);
}
if ($this->rowCount == 2) {
@ -109,10 +130,10 @@ class Matrix
// | a b |
// | c d |
// The determinant is ad - bc
$a = $this->matrixRowData[0][0];
$b = $this->matrixRowData[0][1];
$c = $this->matrixRowData[1][0];
$d = $this->matrixRowData[1][1];
$a = $this->getValue(0, 0);
$b = $this->getValue(0, 1);
$c = $this->getValue(1, 0);
$d = $this->getValue(1, 1);
return $a * $d - $b * $c;
}
@ -126,8 +147,8 @@ class Matrix
$result = 0.0;
// I expand along the first row
for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) {
$firstRowColValue = $this->matrixRowData[0][$currentColumn];
for ($currentColumn = 0; $currentColumn < $this->columnCount; ++$currentColumn) {
$firstRowColValue = $this->getValue(0, $currentColumn);
$cofactor = $this->getCofactor(0, $currentColumn);
$itemToAdd = $firstRowColValue * $cofactor;
$result += $itemToAdd;
@ -152,10 +173,10 @@ class Matrix
// | d -b |
// | -c a |
$a = $this->matrixRowData[0][0];
$b = $this->matrixRowData[0][1];
$c = $this->matrixRowData[1][0];
$d = $this->matrixRowData[1][1];
$a = $this->getValue(0, 0);
$b = $this->getValue(0, 1);
$c = $this->getValue(1, 0);
$d = $this->getValue(1, 1);
return new SquareMatrix(
$d,
@ -168,8 +189,8 @@ class Matrix
// The idea is that it's the transpose of the cofactors
$result = [];
for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) {
for ($currentRow = 0; $currentRow < $this->rowCount; $currentRow++) {
for ($currentColumn = 0; $currentColumn < $this->columnCount; ++$currentColumn) {
for ($currentRow = 0; $currentRow < $this->rowCount; ++$currentRow) {
$result[$currentColumn][$currentRow] = $this->getCofactor($currentRow, $currentColumn);
}
}
@ -180,7 +201,7 @@ class Matrix
public function getInverse(): Matrix|SquareMatrix
{
if (($this->rowCount == 1) && ($this->columnCount == 1)) {
return new SquareMatrix(1.0 / $this->matrixRowData[0][0]);
return new SquareMatrix(1.0 / $this->getValue(0, 0));
}
// Take the simple approach:
@ -197,8 +218,8 @@ class Matrix
$columns = $matrix->getColumnCount();
$newValues = [];
for ($currentRow = 0; $currentRow < $rows; $currentRow++) {
for ($currentColumn = 0; $currentColumn < $columns; $currentColumn++) {
for ($currentRow = 0; $currentRow < $rows; ++$currentRow) {
for ($currentColumn = 0; $currentColumn < $columns; ++$currentColumn) {
$newValues[$currentRow][$currentColumn] = $scalarValue * $matrix->getValue($currentRow, $currentColumn);
}
}
@ -208,7 +229,7 @@ class Matrix
public static function add(Matrix $left, Matrix $right): Matrix
{
if (($left->getRowCount() != $right->getRowCount()) || ($left->getColumnCount() != $right->getColumnCount())) {
if (($left->getRowCount() !== $right->getRowCount()) || ($left->getColumnCount() !== $right->getColumnCount())) {
throw new Exception('Matrices must be of the same size');
}
@ -216,8 +237,8 @@ class Matrix
$resultMatrix = [];
for ($currentRow = 0; $currentRow < $left->getRowCount(); $currentRow++) {
for ($currentColumn = 0; $currentColumn < $right->getColumnCount(); $currentColumn++) {
for ($currentRow = 0; $currentRow < $left->getRowCount(); ++$currentRow) {
for ($currentColumn = 0; $currentColumn < $right->getColumnCount(); ++$currentColumn) {
$resultMatrix[$currentRow][$currentColumn] =
$left->getValue($currentRow, $currentColumn)
+
@ -233,7 +254,7 @@ class Matrix
// Just your standard matrix multiplication.
// See http://en.wikipedia.org/wiki/Matrix_multiplication for details
if ($left->getColumnCount() != $right->getRowCount()) {
if ($left->getColumnCount() !== $right->getRowCount()) {
throw new Exception('The width of the left matrix must match the height of the right matrix');
}
@ -242,11 +263,11 @@ class Matrix
$resultMatrix = [];
for ($currentRow = 0; $currentRow < $resultRows; $currentRow++) {
for ($currentColumn = 0; $currentColumn < $resultColumns; $currentColumn++) {
for ($currentRow = 0; $currentRow < $resultRows; ++$currentRow) {
for ($currentColumn = 0; $currentColumn < $resultColumns; ++$currentColumn) {
$productValue = 0;
for ($vectorIndex = 0; $vectorIndex < $left->getColumnCount(); $vectorIndex++) {
for ($vectorIndex = 0; $vectorIndex < $left->getColumnCount(); ++$vectorIndex) {
$leftValue = $left->getValue($currentRow, $vectorIndex);
$rightValue = $right->getValue($vectorIndex, $currentColumn);
$vectorIndexProduct = $leftValue * $rightValue;
@ -262,6 +283,7 @@ class Matrix
private function getMinorMatrix(int $rowToRemove, int $columnToRemove): Matrix
{
$this->checkRowCol($rowToRemove, $columnToRemove);
// See http://en.wikipedia.org/wiki/Minor_(linear_algebra)
// I'm going to use a horribly naïve algorithm... because I can :)
@ -269,24 +291,24 @@ class Matrix
$actualRow = 0;
for ($currentRow = 0; $currentRow < $this->rowCount; $currentRow++) {
for ($currentRow = 0; $currentRow < $this->rowCount; ++$currentRow) {
if ($currentRow == $rowToRemove) {
continue;
}
$actualCol = 0;
for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) {
for ($currentColumn = 0; $currentColumn < $this->columnCount; ++$currentColumn) {
if ($currentColumn == $columnToRemove) {
continue;
}
$result[$actualRow][$actualCol] = $this->matrixRowData[$currentRow][$currentColumn];
$result[$actualRow][$actualCol] = $this->getValue($currentRow, $currentColumn);
$actualCol++;
++$actualCol;
}
$actualRow++;
++$actualRow;
}
return new Matrix($this->rowCount - 1, $this->columnCount - 1, $result);
@ -294,6 +316,7 @@ class Matrix
public function getCofactor(int $rowToRemove, int $columnToRemove): float
{
$this->checkRowCol($rowToRemove, $columnToRemove);
// See http://en.wikipedia.org/wiki/Cofactor_(linear_algebra) for details
// REVIEW: should things be reversed since I'm 0 indexed?
$sum = $rowToRemove + $columnToRemove;
@ -301,22 +324,22 @@ class Matrix
if ($isEven) {
return $this->getMinorMatrix($rowToRemove, $columnToRemove)->getDeterminant();
} else {
return -1.0 * $this->getMinorMatrix($rowToRemove, $columnToRemove)->getDeterminant();
}
return -1.0 * $this->getMinorMatrix($rowToRemove, $columnToRemove)->getDeterminant();
}
public function equals(Matrix $otherMatrix): bool
{
if (($this->rowCount != $otherMatrix->getRowCount()) || ($this->columnCount != $otherMatrix->getColumnCount())) {
if (($this->rowCount !== $otherMatrix->getRowCount()) || ($this->columnCount !== $otherMatrix->getColumnCount())) {
return FALSE;
}
for ($currentRow = 0; $currentRow < $this->rowCount; $currentRow++) {
for ($currentColumn = 0; $currentColumn < $this->columnCount; $currentColumn++) {
for ($currentRow = 0; $currentRow < $this->rowCount; ++$currentRow) {
for ($currentColumn = 0; $currentColumn < $this->columnCount; ++$currentColumn) {
$delta =
abs(
$this->matrixRowData[$currentRow][$currentColumn] -
$this->getValue($currentRow, $currentColumn) -
$otherMatrix->getValue($currentRow, $currentColumn)
);

View File

@ -11,7 +11,7 @@ use Exception;
class Range
{
final public function __construct(private int $min, private int $max)
final public function __construct(private readonly int $min, private readonly int $max)
{
if ($min > $max) {
throw new Exception('min > max');

View File

@ -13,8 +13,8 @@ class SquareMatrix extends Matrix
$matrixData = [];
$allValuesIndex = 0;
for ($currentRow = 0; $currentRow < $size; $currentRow++) {
for ($currentColumn = 0; $currentColumn < $size; $currentColumn++) {
for ($currentRow = 0; $currentRow < $size; ++$currentRow) {
for ($currentColumn = 0; $currentColumn < $size; ++$currentColumn) {
$matrixData[$currentRow][$currentColumn] = $allValues[$allValuesIndex++];
}
}

View File

@ -15,6 +15,7 @@ class Vector extends Matrix
foreach ($vectorValues as $currentVectorValue) {
$columnValues[] = [$currentVectorValue];
}
parent::__construct(count($vectorValues), 1, $columnValues);
}
}

View File

@ -8,14 +8,14 @@ class PartialPlay
{
public static function getPartialPlayPercentage(Player $player): float
{
$partialPlayPercentage = $player->getPartialPlayPercentage();
$partialPlayPct = $player->getPartialPlayPercentage();
// HACK to get around bug near 0
$smallestPercentage = 0.0001;
if ($partialPlayPercentage < $smallestPercentage) {
$partialPlayPercentage = $smallestPercentage;
$smallestPct = 0.0001;
if ($partialPlayPct < $smallestPct) {
return $smallestPct;
}
return $partialPlayPercentage;
return $partialPlayPct;
}
}

View File

@ -7,34 +7,33 @@ namespace DNW\Skills;
/**
* Represents a player who has a Rating.
*/
class Player implements ISupportPartialPlay, ISupportPartialUpdate, \Stringable
class Player implements ISupportPartialPlay, ISupportPartialUpdate
{
private const DEFAULT_PARTIAL_PLAY_PERCENTAGE = 1.0; // = 100% play time
private const DEFAULT_PARTIAL_UPDATE_PERCENTAGE = 1.0;
private float $PartialPlayPercentage;
private readonly float $PartialPlayPct;
private float $PartialUpdatePercentage;
private readonly float $PartialUpdatePct;
/**
* Constructs a player.
*
* @param mixed $Id The identifier for the player, such as a name.
* @param float $partialPlayPercentage The weight percentage to give this player when calculating a new rank.
* @param float $partialUpdatePercentage Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update.
* @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 mixed $Id,
float $partialPlayPercentage = self::DEFAULT_PARTIAL_PLAY_PERCENTAGE,
float $partialUpdatePercentage = self::DEFAULT_PARTIAL_UPDATE_PERCENTAGE
private readonly mixed $Id,
float $partialPlayPct = self::DEFAULT_PARTIAL_PLAY_PERCENTAGE,
float $partialUpdatePct = self::DEFAULT_PARTIAL_UPDATE_PERCENTAGE
)
{
// If they don't want to give a player an id, that's ok...
Guard::argumentInRangeInclusive($partialPlayPercentage, 0.0, 1.0, 'partialPlayPercentage');
Guard::argumentInRangeInclusive($partialUpdatePercentage, 0, 1.0, 'partialUpdatePercentage');
$this->PartialPlayPercentage = $partialPlayPercentage;
$this->PartialUpdatePercentage = $partialUpdatePercentage;
Guard::argumentInRangeInclusive($partialPlayPct, 0.0, 1.0, 'partialPlayPercentage');
Guard::argumentInRangeInclusive($partialUpdatePct, 0, 1.0, 'partialUpdatePercentage');
$this->PartialPlayPct = $partialPlayPct;
$this->PartialUpdatePct = $partialUpdatePct;
}
/**
@ -50,7 +49,7 @@ class Player implements ISupportPartialPlay, ISupportPartialUpdate, \Stringable
*/
public function getPartialPlayPercentage(): float
{
return $this->PartialPlayPercentage;
return $this->PartialPlayPct;
}
/**
@ -58,11 +57,6 @@ class Player implements ISupportPartialPlay, ISupportPartialUpdate, \Stringable
*/
public function getPartialUpdatePercentage(): float
{
return $this->PartialUpdatePercentage;
}
public function __toString(): string
{
return (string)$this->Id;
return $this->PartialUpdatePct;
}
}

View File

@ -8,8 +8,4 @@ use DNW\Skills\Numerics\Range;
class PlayersRange extends Range
{
protected static function create(int $min, int $max): static
{
return new static($min, $max);
}
}

View File

@ -4,10 +4,12 @@ declare(strict_types=1);
namespace DNW\Skills;
// Container for a player's rating.
use DNW\Skills\Numerics\GaussianDistribution;
class Rating implements \Stringable
/**
* Container for a player's rating.
*/
class Rating
{
private const CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER = 3;
@ -18,7 +20,7 @@ class Rating implements \Stringable
* @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.
*/
public function __construct(private float $mean, private float $standardDeviation, private float|int $conservativeStandardDeviationMultiplier = self::CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER)
public function __construct(private readonly float $mean, private readonly float $standardDeviation, private readonly float|int $conservativeStandardDeviationMultiplier = self::CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER)
{
}
@ -46,6 +48,9 @@ class Rating implements \Stringable
return $this->mean - $this->conservativeStandardDeviationMultiplier * $this->standardDeviation;
}
/**
* Get a partial rating update.
*/
public function getPartialUpdate(Rating $prior, Rating $fullPosterior, float $updatePercentage): Rating
{
$priorGaussian = new GaussianDistribution($prior->getMean(), $prior->getStandardDeviation());
@ -68,9 +73,4 @@ class Rating implements \Stringable
return new Rating($partialPosteriorGaussion->getMean(), $partialPosteriorGaussion->getStandardDeviation(), $prior->conservativeStandardDeviationMultiplier);
}
public function __toString(): string
{
return sprintf('mean=%.4f, standardDeviation=%.4f', $this->mean, $this->standardDeviation);
}
}

View File

@ -6,7 +6,7 @@ namespace DNW\Skills;
class RatingContainer
{
private HashMap $playerToRating;
private readonly HashMap $playerToRating;
public function __construct()
{

View File

@ -11,8 +11,14 @@ use Exception;
*/
abstract class SkillCalculator
{
public const NONE = 0x00;
public const PARTIAL_PLAY = 0x01;
public const PARTIAL_UPDATE = 0x02;
protected function __construct(
private int $supportedOptions,
private readonly int $supportedOptions,
private readonly TeamsRange $totalTeamsAllowed,
private readonly PlayersRange $playersPerTeamAllowed
)
@ -46,7 +52,7 @@ abstract class SkillCalculator
public function isSupported(int $option): bool
{
return (bool)($this->supportedOptions & $option) == $option;
return ($this->supportedOptions & $option) === $option;
}
/**
@ -58,7 +64,7 @@ abstract class SkillCalculator
}
/**
* @param array<\DNW\Skills\Team> $teams
* @param Team[] $teams
*
* @throws \Exception
*/
@ -70,7 +76,8 @@ abstract class SkillCalculator
if (! $playersPerTeam->isInRange($currentTeam->count())) {
throw new Exception('Player count is not in range');
}
$countOfTeams++;
++$countOfTeams;
}
if (! $totalTeams->isInRange($countOfTeams)) {

View File

@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace DNW\Skills;
class SkillCalculatorSupportedOptions
{
public const NONE = 0x00;
public const PARTIAL_PLAY = 0x01;
public const PARTIAL_UPDATE = 0x02;
}

View File

@ -9,10 +9,15 @@ class Team extends RatingContainer
public function __construct(Player $player = NULL, Rating $rating = NULL)
{
parent::__construct();
if ($player && $rating) {
$this->addPlayer($player, $rating);
if (! $player instanceof Player) {
return;
}
if (! $rating instanceof Rating) {
return;
}
$this->addPlayer($player, $rating);
}
public function addPlayer(Player $player, Rating $rating): self

View File

@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace DNW\Skills;
class Teams
{
/**
* @return Team[]
*/
public static function concat(Team ...$args/*variable arguments*/): array
{
$result = [];
foreach ($args as $currentTeam) {
$localCurrentTeam = $currentTeam;
$result[] = $localCurrentTeam;
}
return $result;
}
}

View File

@ -8,8 +8,4 @@ use DNW\Skills\Numerics\Range;
class TeamsRange extends Range
{
protected static function create(int $min, int $max): static
{
return new static($min, $max);
}
}

View File

@ -14,7 +14,6 @@ use DNW\Skills\PartialPlay;
use DNW\Skills\PlayersRange;
use DNW\Skills\RankSorter;
use DNW\Skills\SkillCalculator;
use DNW\Skills\SkillCalculatorSupportedOptions;
use DNW\Skills\Team;
use DNW\Skills\TeamsRange;
use DNW\Skills\RatingContainer;
@ -27,8 +26,9 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
{
public function __construct()
{
parent::__construct(SkillCalculatorSupportedOptions::PARTIAL_PLAY | SkillCalculatorSupportedOptions::PARTIAL_UPDATE, TeamsRange::atLeast(2), PlayersRange::atLeast(1));
parent::__construct(SkillCalculator::PARTIAL_PLAY | SkillCalculator::PARTIAL_UPDATE, TeamsRange::atLeast(2), PlayersRange::atLeast(1));
}
/**
* {@inheritdoc}
*/
@ -38,7 +38,6 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
array $teamRanks
): RatingContainer
{
Guard::argumentNotNull($gameInfo, 'gameInfo');
$this->validateTeamCountAndPlayersCountPerTeam($teams);
RankSorter::sort($teams, $teamRanks);
@ -51,6 +50,7 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
return $factorGraph->getUpdatedRatings();
}
/**
* {@inheritdoc}
*/
@ -104,7 +104,7 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
return new Vector(
self::getPlayerRatingValues(
$teamAssignmentsList,
fn (Rating $rating): float => $rating->getMean()
static fn(Rating $rating): float => $rating->getMean()
)
);
}
@ -119,7 +119,7 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
return new DiagonalMatrix(
self::getPlayerRatingValues(
$teamAssignmentsList,
fn (Rating $rating): float => BasicMath::square($rating->getStandardDeviation())
static fn(Rating $rating): float => BasicMath::square($rating->getStandardDeviation())
)
);
}
@ -171,7 +171,8 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
$currentColumn = 0;
for ($i = 0; $i < count($teamAssignmentsList) - 1; $i++) {
$teamCnt = count($teamAssignmentsList);
for ($i = 0; $i < $teamCnt - 1; ++$i) {
$currentTeam = $teamAssignmentsList[$i];
// Need to add in 0's for all the previous players, since they're not
@ -181,7 +182,7 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
foreach ($currentTeam->getAllPlayers() as $currentPlayer) {
$playerAssignments[$currentColumn][] = PartialPlay::getPartialPlayPercentage($currentPlayer);
// indicates the player is on the team
$totalPreviousPlayers++;
++$totalPreviousPlayers;
}
$rowsRemaining = $totalPlayers - $totalPreviousPlayers;
@ -190,15 +191,15 @@ class FactorGraphTrueSkillCalculator extends SkillCalculator
foreach ($nextTeam->getAllPlayers() as $nextTeamPlayer) {
// Add a -1 * playing time to represent the difference
$playerAssignments[$currentColumn][] = -1 * PartialPlay::getPartialPlayPercentage($nextTeamPlayer);
$rowsRemaining--;
--$rowsRemaining;
}
for ($ixAdditionalRow = 0; $ixAdditionalRow < $rowsRemaining; $ixAdditionalRow++) {
for ($ixAdditionalRow = 0; $ixAdditionalRow < $rowsRemaining; ++$ixAdditionalRow) {
// Pad with zeros
$playerAssignments[$currentColumn][] = 0;
}
$currentColumn++;
++$currentColumn;
}
return Matrix::fromColumnValues($totalPlayers, count($teamAssignmentsList) - 1, $playerAssignments);

View File

@ -32,7 +32,6 @@ abstract class GaussianFactor extends Factor
$variable,
new Message(
$newDistribution,
sprintf('message from %s to %s', (string)$this, (string)$variable)
)
);
}

View File

@ -16,12 +16,9 @@ use DNW\Skills\TrueSkill\TruncatedGaussianCorrectionFunctions;
*/
class GaussianGreaterThanFactor extends GaussianFactor
{
private float $epsilon;
public function __construct(float $epsilon, Variable $variable)
public function __construct(private readonly float $epsilon, Variable $variable)
{
parent::__construct(\sprintf('%s > %.2f', (string)$variable, $epsilon));
$this->epsilon = $epsilon;
parent::__construct();
$this->createVariableToMessageBinding($variable);
}

View File

@ -17,11 +17,12 @@ use Exception;
*/
class GaussianLikelihoodFactor extends GaussianFactor
{
private float $precision;
private readonly float $precision;
public function __construct(float $betaSquared, Variable $variable1, Variable $variable2)
{
parent::__construct(sprintf('Likelihood of %s going to %s', (string)$variable2, (string)$variable1));
//Likelihood of $variable1 going to $variable2
parent::__construct();
$this->precision = 1.0 / $betaSquared;
$this->createVariableToMessageBinding($variable1);
$this->createVariableToMessageBinding($variable2);
@ -59,9 +60,9 @@ class GaussianLikelihoodFactor extends GaussianFactor
$a * ($marginal2->getPrecision() - $message2Value->getPrecision())
);
$oldMarginalWithoutMessage = GaussianDistribution::divide($marginal1, $message1Value);
$oldMarginalWithoutMsg = GaussianDistribution::divide($marginal1, $message1Value);
$newMarginal = GaussianDistribution::multiply($oldMarginalWithoutMessage, $newMessage);
$newMarginal = GaussianDistribution::multiply($oldMarginalWithoutMsg, $newMessage);
// Update the message and marginal

View File

@ -15,15 +15,15 @@ use DNW\Skills\Numerics\GaussianDistribution;
*/
class GaussianPriorFactor extends GaussianFactor
{
private GaussianDistribution $newMessage;
private readonly GaussianDistribution $newMessage;
public function __construct(float $mean, float $variance, Variable $variable)
{
parent::__construct(sprintf('Prior value going to %s', (string)$variable));
//Prior value going to $variable
parent::__construct();
$this->newMessage = new GaussianDistribution($mean, sqrt($variance));
$newMessage = new Message(
GaussianDistribution::fromPrecisionMean(0, 0),
sprintf('message from %s to %s', (string)$this, (string)$variable)
GaussianDistribution::fromPrecisionMean(0, 0)
);
$this->createVariableToMessageBindingWithMessage($variable, $newMessage);

View File

@ -19,9 +19,9 @@ use DNW\Skills\Numerics\GaussianDistribution;
class GaussianWeightedSumFactor extends GaussianFactor
{
/**
* @var array<int[]> $variableIndexOrdersForWeights
* @var array<int[]> $varIndexOrdersForWeights
*/
private array $variableIndexOrdersForWeights = [];
private array $varIndexOrdersForWeights = [];
/**
* This following is used for convenience, for example, the first entry is [0, 1, 2]
@ -30,6 +30,7 @@ class GaussianWeightedSumFactor extends GaussianFactor
* @var array<float[]> $weights
*/
private array $weights = [];
/**
* @var array<float[]> $weightsSquared
*/
@ -41,14 +42,14 @@ class GaussianWeightedSumFactor extends GaussianFactor
*/
public function __construct(Variable $sumVariable, array $variablesToSum, array $variableWeights)
{
parent::__construct(self::createName((string)$sumVariable, $variablesToSum, $variableWeights));
parent::__construct();
// The first weights are a straightforward copy
// v_0 = a_1*v_1 + a_2*v_2 + ... + a_n * v_n
$variableWeightsLength = count($variableWeights);
$this->weights[0] = array_fill(0, count($variableWeights), 0);
for ($i = 0; $i < $variableWeightsLength; $i++) {
for ($i = 0; $i < $variableWeightsLength; ++$i) {
$weight = &$variableWeights[$i];
$this->weights[0][$i] = $weight;
$this->weightsSquared[0][$i] = BasicMath::square($weight);
@ -57,9 +58,9 @@ class GaussianWeightedSumFactor extends GaussianFactor
$variablesToSumLength = count($variablesToSum);
// 0..n-1
$this->variableIndexOrdersForWeights[0] = [];
for ($i = 0; $i < ($variablesToSumLength + 1); $i++) {
$this->variableIndexOrdersForWeights[0][] = $i;
$this->varIndexOrdersForWeights[0] = [];
for ($i = 0; $i < ($variablesToSumLength + 1); ++$i) {
$this->varIndexOrdersForWeights[0][] = $i;
}
$variableWeightsLength = count($variableWeights);
@ -70,7 +71,7 @@ class GaussianWeightedSumFactor extends GaussianFactor
// By convention, we'll put the v_0 term at the end
$weightsLength = $variableWeightsLength + 1;
for ($weightsIndex = 1; $weightsIndex < $weightsLength; $weightsIndex++) {
for ($weightsIndex = 1; $weightsIndex < $weightsLength; ++$weightsIndex) {
$currentWeights = \array_fill(0, $variableWeightsLength, 0);
$variableIndices = \array_fill(0, $variableWeightsLength + 1, 0);
@ -82,7 +83,7 @@ class GaussianWeightedSumFactor extends GaussianFactor
// This is helpful since we skip over one of the spots
$currentDestinationWeightIndex = 0;
for ($currentWeightSourceIndex = 0; $currentWeightSourceIndex < $variableWeightsLength; $currentWeightSourceIndex++) {
for ($currentWeightSourceIndex = 0; $currentWeightSourceIndex < $variableWeightsLength; ++$currentWeightSourceIndex) {
if ($currentWeightSourceIndex === $weightsIndex - 1) {
continue;
}
@ -98,7 +99,7 @@ class GaussianWeightedSumFactor extends GaussianFactor
$currentWeightsSquared[$currentDestinationWeightIndex] = $currentWeight * $currentWeight;
$variableIndices[$currentDestinationWeightIndex + 1] = $currentWeightSourceIndex + 1;
$currentDestinationWeightIndex++;
++$currentDestinationWeightIndex;
}
// And the final one
@ -108,10 +109,11 @@ class GaussianWeightedSumFactor extends GaussianFactor
// HACK: Getting around division by zero
$finalWeight = 0;
}
$currentWeights[$currentDestinationWeightIndex] = $finalWeight;
$currentWeightsSquared[$currentDestinationWeightIndex] = BasicMath::square($finalWeight);
$variableIndices[count($variableWeights)] = 0;
$this->variableIndexOrdersForWeights[] = $variableIndices;
$this->varIndexOrdersForWeights[] = $variableIndices;
$this->weights[$weightsIndex] = $currentWeights;
$this->weightsSquared[$weightsIndex] = $currentWeightsSquared;
@ -125,15 +127,17 @@ class GaussianWeightedSumFactor extends GaussianFactor
}
}
public function getLogNormalization()
public function getLogNormalization(): float
{
$vars = $this->getVariables();
$messages = $this->getMessages();
$result = 0.0;
// We start at 1 since offset 0 has the sum
$counter = count($vars);
// We start at 1 since offset 0 has the sum
for ($i = 1; $i < count($vars); $i++) {
for ($i = 1; $i < $counter; ++$i) {
$result += GaussianDistribution::logRatioNormalization($vars[$i]->getValue(), $messages[$i]->getValue());
}
@ -156,28 +160,21 @@ class GaussianWeightedSumFactor extends GaussianFactor
// The math works out so that 1/newPrecision = sum of a_i^2 /marginalsWithoutMessages[i]
$inverseOfNewPrecisionSum = 0.0;
$anotherInverseOfNewPrecisionSum = 0.0;
$weightedMeanSum = 0.0;
$anotherWeightedMeanSum = 0.0;
$weightsSquaredLength = count($weightsSquared);
for ($i = 0; $i < $weightsSquaredLength; $i++) {
for ($i = 0; $i < $weightsSquaredLength; ++$i) {
// These flow directly from the paper
$inverseOfNewPrecisionSum += $weightsSquared[$i] /
($variables[$i + 1]->getValue()->getPrecision() - $messages[$i + 1]->getValue()->getPrecision());
$diff = GaussianDistribution::divide($variables[$i + 1]->getValue(), $messages[$i + 1]->getValue());
$anotherInverseOfNewPrecisionSum += $weightsSquared[$i] / $diff->getPrecision();
$weightedMeanSum += $weights[$i]
*
($variables[$i + 1]->getValue()->getPrecisionMean() - $messages[$i + 1]->getValue()->getPrecisionMean())
/
($variables[$i + 1]->getValue()->getPrecision() - $messages[$i + 1]->getValue()->getPrecision());
$anotherWeightedMeanSum += $weights[$i] * $diff->getPrecisionMean() / $diff->getPrecision();
}
$newPrecision = 1.0 / $inverseOfNewPrecisionSum;
@ -210,12 +207,16 @@ class GaussianWeightedSumFactor extends GaussianFactor
$updatedMessages = [];
$updatedVariables = [];
$indicesToUse = $this->variableIndexOrdersForWeights[$messageIndex];
$indicesToUse = $this->varIndexOrdersForWeights[$messageIndex];
// The tricky part here is that we have to put the messages and variables in the same
// order as the weights. Thankfully, the weights and messages share the same index numbers,
// so we just need to make sure they're consistent
$counter = count($allMessages);
// The tricky part here is that we have to put the messages and variables in the same
// order as the weights. Thankfully, the weights and messages share the same index numbers,
// so we just need to make sure they're consistent
for ($i = 0; $i < count($allMessages); $i++) {
for ($i = 0; $i < $counter; ++$i) {
$updatedMessages[] = $allMessages[$indicesToUse[$i]];
$updatedVariables[] = $allVariables[$indicesToUse[$i]];
}
@ -227,42 +228,4 @@ class GaussianWeightedSumFactor extends GaussianFactor
$updatedVariables
);
}
/**
* @param Variable[] $variablesToSum
* @param float[] $weights
*/
private static function createName(string $sumVariable, array $variablesToSum, array $weights): string
{
// TODO: Perf? Use PHP equivalent of StringBuilder? implode on arrays?
$result = $sumVariable;
$result .= ' = ';
$totalVars = count($variablesToSum);
for ($i = 0; $i < $totalVars; $i++) {
$isFirst = ($i == 0);
if ($isFirst && ($weights[$i] < 0)) {
$result .= '-';
}
$absValue = sprintf('%.2f', \abs($weights[$i])); // 0.00?
$result .= $absValue;
$result .= '*[';
$result .= (string)$variablesToSum[$i];
$result .= ']';
$isLast = ($i === $totalVars - 1);
if (! $isLast) {
if ($weights[$i + 1] >= 0) {
$result .= ' + ';
} else {
$result .= ' - ';
}
}
}
return $result;
}
}

View File

@ -16,12 +16,10 @@ use DNW\Skills\TrueSkill\TruncatedGaussianCorrectionFunctions;
*/
class GaussianWithinFactor extends GaussianFactor
{
private float $epsilon;
public function __construct(float $epsilon, Variable $variable)
public function __construct(private readonly float $epsilon, Variable $variable)
{
parent::__construct(sprintf('%s <= %.2f', (string)$variable, $epsilon));
$this->epsilon = $epsilon;
//$epsilon <= $variable
parent::__construct();
$this->createVariableToMessageBinding($variable);
}

View File

@ -15,8 +15,8 @@ class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
{
public function __construct(
TrueSkillFactorGraph $parentGraph,
private readonly TeamPerformancesToTeamPerformanceDifferencesLayer $TeamPerformancesToTeamPerformanceDifferencesLayer,
private readonly TeamDifferencesComparisonLayer $TeamDifferencesComparisonLayer
private readonly TeamPerformancesToTeamPerformanceDifferencesLayer $teamPerformancesToTeamPerformanceDifferencesLayer,
private readonly TeamDifferencesComparisonLayer $teamDifferencesComparisonLayer
)
{
parent::__construct($parentGraph);
@ -25,20 +25,20 @@ class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
public function getLocalFactors(): array
{
return array_merge(
$this->TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors(),
$this->TeamDifferencesComparisonLayer->getLocalFactors()
$this->teamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors(),
$this->teamDifferencesComparisonLayer->getLocalFactors()
);
}
public function buildLayer(): void
{
$inputVariablesGroups = $this->getInputVariablesGroups();
$this->TeamPerformancesToTeamPerformanceDifferencesLayer->setInputVariablesGroups($inputVariablesGroups);
$this->TeamPerformancesToTeamPerformanceDifferencesLayer->buildLayer();
$this->teamPerformancesToTeamPerformanceDifferencesLayer->setInputVariablesGroups($inputVariablesGroups);
$this->teamPerformancesToTeamPerformanceDifferencesLayer->buildLayer();
$teamDifferencesOutputVariablesGroups = $this->TeamPerformancesToTeamPerformanceDifferencesLayer->getOutputVariablesGroups();
$this->TeamDifferencesComparisonLayer->setInputVariablesGroups($teamDifferencesOutputVariablesGroups);
$this->TeamDifferencesComparisonLayer->buildLayer();
$teamDifferencesOutputVariablesGroups = $this->teamPerformancesToTeamPerformanceDifferencesLayer->getOutputVariablesGroups();
$this->teamDifferencesComparisonLayer->setInputVariablesGroups($teamDifferencesOutputVariablesGroups);
$this->teamDifferencesComparisonLayer->buildLayer();
}
public function createPriorSchedule(): ?ScheduleSequence
@ -56,24 +56,24 @@ class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
}
// When dealing with differences, there are always (n-1) differences, so add in the 1
$totalTeamDifferences = count($this->TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors());
$totalTeamDifferences = count($this->teamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors());
$localFactors = $this->TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors();
$localFactors = $this->teamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors();
$firstDifferencesFactor = $localFactors[0];
$lastDifferencesFactor = $localFactors[$totalTeamDifferences - 1];
//inner schedule
return new ScheduleSequence(
'inner schedule',
[
$loop,
//teamPerformanceToPerformanceDifferenceFactors[0] @ 1
new ScheduleStep(
'teamPerformanceToPerformanceDifferenceFactors[0] @ 1',
$firstDifferencesFactor,
1
),
//teamPerformanceToPerformanceDifferenceFactors[teamTeamDifferences = %d - 1] @ 2
new ScheduleStep(
sprintf('teamPerformanceToPerformanceDifferenceFactors[teamTeamDifferences = %d - 1] @ 2', $totalTeamDifferences),
$lastDifferencesFactor,
2
),
@ -83,95 +83,96 @@ class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
private function createTwoTeamInnerPriorLoopSchedule(): ScheduleSequence
{
$teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors = $this->TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors();
$teamDifferencesComparisonLayerLocalFactors = $this->TeamDifferencesComparisonLayer->getLocalFactors();
$teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors = $this->teamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors();
$teamDifferencesComparisonLayerLocalFactors = $this->teamDifferencesComparisonLayer->getLocalFactors();
$firstPerfToTeamDiff = $teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors[0];
$firstTeamDiffComparison = $teamDifferencesComparisonLayerLocalFactors[0];
$itemsToSequence = [
//send team perf to perf differences
new ScheduleStep(
'send team perf to perf differences',
$firstPerfToTeamDiff,
0
),
//send to greater than or within factor
new ScheduleStep(
'send to greater than or within factor',
$firstTeamDiffComparison,
0
),
];
//loop of just two teams inner sequence
return $this->scheduleSequence(
$itemsToSequence,
'loop of just two teams inner sequence'
$itemsToSequence
);
}
private function createMultipleTeamInnerPriorLoopSchedule(): ScheduleLoop
{
$totalTeamDifferences = count($this->TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors());
$totalTeamDifferences = count($this->teamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors());
$forwardScheduleList = [];
for ($i = 0; $i < $totalTeamDifferences - 1; $i++) {
$teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors = $this->TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors();
$teamDifferencesComparisonLayerLocalFactors = $this->TeamDifferencesComparisonLayer->getLocalFactors();
for ($i = 0; $i < $totalTeamDifferences - 1; ++$i) {
$teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors = $this->teamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors();
$teamDifferencesComparisonLayerLocalFactors = $this->teamDifferencesComparisonLayer->getLocalFactors();
$currentTeamPerfToTeamPerfDiff = $teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors[$i];
$currentTeamDiffComparison = $teamDifferencesComparisonLayerLocalFactors[$i];
//current forward schedule piece $i
$currentForwardSchedulePiece =
$this->scheduleSequence(
[
//team perf to perf diff
new ScheduleStep(
sprintf('team perf to perf diff %d', $i),
$currentTeamPerfToTeamPerfDiff,
0
),
//greater than or within result factor
new ScheduleStep(
sprintf('greater than or within result factor %d', $i),
$currentTeamDiffComparison,
0
),
//'team perf to perf diff factors
new ScheduleStep(
sprintf('team perf to perf diff factors [%d], 2', $i),
$currentTeamPerfToTeamPerfDiff,
2
),
],
sprintf('current forward schedule piece %d', $i)
]
);
$forwardScheduleList[] = $currentForwardSchedulePiece;
}
$forwardSchedule = new ScheduleSequence('forward schedule', $forwardScheduleList);
//forward schedule
$forwardSchedule = new ScheduleSequence($forwardScheduleList);
$backwardScheduleList = [];
for ($i = 0; $i < $totalTeamDifferences - 1; $i++) {
$teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors = $this->TeamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors();
$teamDifferencesComparisonLayerLocalFactors = $this->TeamDifferencesComparisonLayer->getLocalFactors();
for ($i = 0; $i < $totalTeamDifferences - 1; ++$i) {
$teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors = $this->teamPerformancesToTeamPerformanceDifferencesLayer->getLocalFactors();
$teamDifferencesComparisonLayerLocalFactors = $this->teamDifferencesComparisonLayer->getLocalFactors();
$differencesFactor = $teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors[$totalTeamDifferences - 1 - $i];
$comparisonFactor = $teamDifferencesComparisonLayerLocalFactors[$totalTeamDifferences - 1 - $i];
$performancesToDifferencesFactor = $teamPerformancesToTeamPerformanceDifferencesLayerLocalFactors[$totalTeamDifferences - 1 - $i];
//current backward schedule piece
$currentBackwardSchedulePiece = new ScheduleSequence(
'current backward schedule piece',
[
//teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - %d] @ 0
new ScheduleStep(
sprintf('teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - %d] @ 0', $i),
$differencesFactor,
0
),
//greaterThanOrWithinResultFactors[totalTeamDifferences - 1 - %d] @ 0
new ScheduleStep(
sprintf('greaterThanOrWithinResultFactors[totalTeamDifferences - 1 - %d] @ 0', $i),
$comparisonFactor,
0
),
//teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - %d] @ 1
new ScheduleStep(
sprintf('teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - %d] @ 1', $i),
$performancesToDifferencesFactor,
1
),
@ -180,18 +181,19 @@ class IteratedTeamDifferencesInnerLayer extends TrueSkillFactorGraphLayer
$backwardScheduleList[] = $currentBackwardSchedulePiece;
}
$backwardSchedule = new ScheduleSequence('backward schedule', $backwardScheduleList);
//backward schedule
$backwardSchedule = new ScheduleSequence($backwardScheduleList);
$forwardBackwardScheduleToLoop =
//forward Backward Schedule To Loop
new ScheduleSequence(
'forward Backward Schedule To Loop',
[$forwardSchedule, $backwardSchedule]
);
$initialMaxDelta = 0.0001;
//loop with max delta
return new ScheduleLoop(
sprintf('loop with max delta of %f', $initialMaxDelta),
$forwardBackwardScheduleToLoop,
$initialMaxDelta
);

View File

@ -7,8 +7,6 @@ namespace DNW\Skills\TrueSkill\Layers;
use DNW\Skills\FactorGraphs\ScheduleStep;
use DNW\Skills\FactorGraphs\ScheduleSequence;
use DNW\Skills\PartialPlay;
use DNW\Skills\Player;
use DNW\Skills\Team;
use DNW\Skills\TrueSkill\Factors\GaussianWeightedSumFactor;
use DNW\Skills\FactorGraphs\Variable;
use DNW\Skills\FactorGraphs\KeyedVariable;
@ -18,16 +16,19 @@ class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLaye
public function buildLayer(): void
{
$inputVariablesGroups = $this->getInputVariablesGroups();
/**
* @var KeyedVariable[] $currentTeam
*/
foreach ($inputVariablesGroups as $currentTeam) {
$localCurrentTeam = $currentTeam;
$teamPerformance = $this->createOutputVariable($localCurrentTeam);
$teamPerformance = $this->createOutputVariable();
$newSumFactor = $this->createPlayerToTeamSumFactor($localCurrentTeam, $teamPerformance);
$this->addLayerFactor($newSumFactor);
// REVIEW: Does it make sense to have groups of one?
$outputVariablesGroups = &$this->getOutputVariablesGroups();
$outputVariablesGroups[] = [$teamPerformance];
$outputVarGroups = &$this->getOutputVariablesGroups();
$outputVarGroups[] = [$teamPerformance];
}
}
@ -35,12 +36,13 @@ class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLaye
{
$localFactors = $this->getLocalFactors();
//all player perf to team perf schedule
return $this->scheduleSequence(
array_map(
fn ($weightedSumFactor) => new ScheduleStep('Perf to Team Perf Step', $weightedSumFactor, 0),
//Perf to Team Perf Step
static fn($weightedSumFactor): ScheduleStep => new ScheduleStep($weightedSumFactor, 0),
$localFactors
),
'all player perf to team perf schedule'
)
);
}
@ -50,9 +52,8 @@ class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLaye
protected function createPlayerToTeamSumFactor(array $teamMembers, Variable $sumVariable): GaussianWeightedSumFactor
{
$weights = array_map(
function ($v) {
static function ($v): float {
$player = $v->getKey();
return PartialPlay::getPartialPlayPercentage($player);
},
$teamMembers
@ -72,27 +73,24 @@ class PlayerPerformancesToTeamPerformancesLayer extends TrueSkillFactorGraphLaye
foreach ($localFactors as $currentFactor) {
$localCurrentFactor = $currentFactor;
$numberOfMessages = $localCurrentFactor->getNumberOfMessages();
for ($currentIteration = 1; $currentIteration < $numberOfMessages; $currentIteration++) {
for ($currentIteration = 1; $currentIteration < $numberOfMessages; ++$currentIteration) {
//team sum perf
$allFactors[] = new ScheduleStep(
'team sum perf @' . $currentIteration,
$localCurrentFactor,
$currentIteration
);
}
}
return $this->scheduleSequence($allFactors, "all of the team's sum iterations");
//all of the team's sum iterations
return $this->scheduleSequence($allFactors);
}
/**
* @param KeyedVariable[] $team
* Team's performance
*/
private function createOutputVariable(array $team): Variable
private function createOutputVariable(): Variable
{
$memberNames = array_map(fn ($currentPlayer) => (string)($currentPlayer->getKey()), $team);
$teamMemberNames = \implode(', ', $memberNames);
return $this->getParentFactorGraph()->getVariableFactory()->createBasicVariable('Team[' . $teamMemberNames . "]'s performance");
return $this->getParentFactorGraph()->getVariableFactory()->createBasicVariable();
}
}

View File

@ -34,12 +34,12 @@ class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
$localCurrentTeam = $currentTeam;
$currentTeamSkills = [];
$currentTeamAllPlayers = $localCurrentTeam->getAllPlayers();
foreach ($currentTeamAllPlayers as $currentTeamPlayer) {
$localCurrentTeamPlayer = $currentTeamPlayer;
$currentTeamPlayerRating = $currentTeam->getRating($localCurrentTeamPlayer);
$playerSkill = $this->createSkillOutputVariable($localCurrentTeamPlayer);
$priorFactor = $this->createPriorFactor($currentTeamPlayerRating, $playerSkill);
$curTeamAllPlayers = $localCurrentTeam->getAllPlayers();
foreach ($curTeamAllPlayers as $curTeamPlayer) {
$localCurTeamPlayer = $curTeamPlayer;
$curTeamPlayerRating = $currentTeam->getRating($localCurTeamPlayer);
$playerSkill = $this->createSkillOutputVariable($localCurTeamPlayer);
$priorFactor = $this->createPriorFactor($curTeamPlayerRating, $playerSkill);
$this->addLayerFactor($priorFactor);
$currentTeamSkills[] = $playerSkill;
}
@ -53,12 +53,13 @@ class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
{
$localFactors = $this->getLocalFactors();
//All priors
return $this->scheduleSequence(
array_map(
fn ($prior) => new ScheduleStep('Prior to Skill Step', $prior, 0),
//Prior to Skill Step
static fn($prior): ScheduleStep => new ScheduleStep($prior, 0),
$localFactors
),
'All priors'
)
);
}
@ -77,6 +78,6 @@ class PlayerPriorValuesToSkillsLayer extends TrueSkillFactorGraphLayer
$parentFactorGraph = $this->getParentFactorGraph();
$variableFactory = $parentFactorGraph->getVariableFactory();
return $variableFactory->createKeyedVariable($key, $key . "'s skill");
return $variableFactory->createKeyedVariable($key);
}
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace DNW\Skills\TrueSkill\Layers;
use DNW\Skills\FactorGraphs\KeyedVariable;
use DNW\Skills\FactorGraphs\Variable;
use DNW\Skills\FactorGraphs\ScheduleStep;
use DNW\Skills\Numerics\BasicMath;
use DNW\Skills\TrueSkill\Factors\GaussianLikelihoodFactor;
@ -14,26 +15,29 @@ class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
{
public function buildLayer(): void
{
$inputVariablesGroups = $this->getInputVariablesGroups();
$outputVariablesGroups = &$this->getOutputVariablesGroups();
$inputVarGroups = $this->getInputVariablesGroups();
$outputVarGroups = &$this->getOutputVariablesGroups();
foreach ($inputVariablesGroups as $currentTeam) {
foreach ($inputVarGroups as $currentTeam) {
$currentTeamPlayerPerformances = [];
foreach ($currentTeam as $playerSkillVariable) {
$localPlayerSkillVariable = $playerSkillVariable;
$currentPlayer = $localPlayerSkillVariable->getKey();
/**
* @var Variable $playerSkillVar
*/
foreach ($currentTeam as $playerSkillVar) {
$localPlayerSkillVar = $playerSkillVar;
$currentPlayer = ($localPlayerSkillVar instanceof KeyedVariable) ? $localPlayerSkillVar->getKey() : "";
$playerPerformance = $this->createOutputVariable($currentPlayer);
$newLikelihoodFactor = $this->createLikelihood($localPlayerSkillVariable, $playerPerformance);
$newLikelihoodFactor = $this->createLikelihood($localPlayerSkillVar, $playerPerformance);
$this->addLayerFactor($newLikelihoodFactor);
$currentTeamPlayerPerformances[] = $playerPerformance;
}
$outputVariablesGroups[] = $currentTeamPlayerPerformances;
$outputVarGroups[] = $currentTeamPlayerPerformances;
}
}
private function createLikelihood(KeyedVariable $playerSkill, KeyedVariable $playerPerformance): GaussianLikelihoodFactor
private function createLikelihood(Variable $playerSkill, Variable $playerPerformance): GaussianLikelihoodFactor
{
return new GaussianLikelihoodFactor(
BasicMath::square($this->getParentFactorGraph()->getGameInfo()->getBeta()),
@ -44,19 +48,20 @@ class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
private function createOutputVariable(mixed $key): KeyedVariable
{
return $this->getParentFactorGraph()->getVariableFactory()->createKeyedVariable($key, $key . "'s performance");
return $this->getParentFactorGraph()->getVariableFactory()->createKeyedVariable($key);
}
public function createPriorSchedule(): ?ScheduleSequence
{
$localFactors = $this->getLocalFactors();
//All skill to performance sending
return $this->scheduleSequence(
array_map(
fn ($likelihood) => new ScheduleStep('Skill to Perf step', $likelihood, 0),
//Skill to Perf step
static fn($likelihood): ScheduleStep => new ScheduleStep($likelihood, 0),
$localFactors
),
'All skill to performance sending'
)
);
}
@ -64,12 +69,12 @@ class PlayerSkillsToPerformancesLayer extends TrueSkillFactorGraphLayer
{
$localFactors = $this->getLocalFactors();
//All skill to performance sending
return $this->scheduleSequence(
array_map(
fn ($likelihood) => new ScheduleStep('name', $likelihood, 1),
static fn($likelihood): ScheduleStep => new ScheduleStep($likelihood, 1),
$localFactors
),
'All skill to performance sending'
)
);
}
}

View File

@ -11,7 +11,7 @@ use DNW\Skills\TrueSkill\TrueSkillFactorGraph;
class TeamDifferencesComparisonLayer extends TrueSkillFactorGraphLayer
{
private float $epsilon;
private readonly float $epsilon;
/**
* @param int[] $teamRanks
@ -28,7 +28,7 @@ class TeamDifferencesComparisonLayer extends TrueSkillFactorGraphLayer
$inputVarGroups = $this->getInputVariablesGroups();
$inputVarGroupsCount = count($inputVarGroups);
for ($i = 0; $i < $inputVarGroupsCount; $i++) {
for ($i = 0; $i < $inputVarGroupsCount; ++$i) {
$isDraw = ($this->teamRanks[$i] == $this->teamRanks[$i + 1]);
$teamDifference = $inputVarGroups[$i][0];

View File

@ -15,7 +15,7 @@ class TeamPerformancesToTeamPerformanceDifferencesLayer extends TrueSkillFactorG
$inputVariablesGroupsCount = count($inputVariablesGroups);
$outputVariablesGroup = &$this->getOutputVariablesGroups();
for ($i = 0; $i < $inputVariablesGroupsCount - 1; $i++) {
for ($i = 0; $i < $inputVariablesGroupsCount - 1; ++$i) {
$strongerTeam = $inputVariablesGroups[$i][0];
$weakerTeam = $inputVariablesGroups[$i + 1][0];
@ -40,8 +40,11 @@ class TeamPerformancesToTeamPerformanceDifferencesLayer extends TrueSkillFactorG
return new GaussianWeightedSumFactor($output, $teams, $weights);
}
/**
* Team performance difference
*/
private function createOutputVariable(): Variable
{
return $this->getParentFactorGraph()->getVariableFactory()->createBasicVariable('Team performance difference');
return $this->getParentFactorGraph()->getVariableFactory()->createBasicVariable();
}
}

View File

@ -12,8 +12,10 @@ use DNW\Skills\GameInfo;
use DNW\Skills\Numerics\GaussianDistribution;
use DNW\Skills\Rating;
use DNW\Skills\Team;
use DNW\Skills\Player;
use DNW\Skills\RatingContainer;
use DNW\Skills\FactorGraphs\FactorGraphLayer;
use DNW\Skills\FactorGraphs\KeyedVariable;
use DNW\Skills\TrueSkill\Layers\IteratedTeamDifferencesInnerLayer;
use DNW\Skills\TrueSkill\Layers\PlayerPerformancesToTeamPerformancesLayer;
use DNW\Skills\TrueSkill\Layers\PlayerPriorValuesToSkillsLayer;
@ -26,9 +28,9 @@ class TrueSkillFactorGraph extends FactorGraph
/**
* @var FactorGraphLayer[] $layers
*/
private array $layers;
private readonly array $layers;
private PlayerPriorValuesToSkillsLayer $priorLayer;
private readonly PlayerPriorValuesToSkillsLayer $priorLayer;
/**
* @param GameInfo $gameInfo Parameters for the game.
@ -37,9 +39,10 @@ class TrueSkillFactorGraph extends FactorGraph
*/
public function __construct(private readonly GameInfo $gameInfo, array $teams, array $teamRanks)
{
parent::__construct();
$this->priorLayer = new PlayerPriorValuesToSkillsLayer($this, $teams);
$newFactory = new VariableFactory(
fn () => GaussianDistribution::fromPrecisionMean(0, 0)
static fn(): GaussianDistribution => GaussianDistribution::fromPrecisionMean(0, 0)
);
$this->setVariableFactory($newFactory);
@ -115,13 +118,14 @@ class TrueSkillFactorGraph extends FactorGraph
$allLayersReverse = array_reverse($this->layers);
foreach ($allLayersReverse as $currentLayer) {
$currentPosteriorSchedule = $currentLayer->createPosteriorSchedule();
if ($currentPosteriorSchedule != NULL) {
$fullSchedule[] = $currentPosteriorSchedule;
$curPosteriorSchedule = $currentLayer->createPosteriorSchedule();
if ($curPosteriorSchedule != NULL) {
$fullSchedule[] = $curPosteriorSchedule;
}
}
return new ScheduleSequence('Full schedule', $fullSchedule);
//Full schedule
return new ScheduleSequence($fullSchedule);
}
public function getUpdatedRatings(): RatingContainer
@ -131,7 +135,7 @@ class TrueSkillFactorGraph extends FactorGraph
$priorLayerOutputVariablesGroups = $this->priorLayer->getOutputVariablesGroups();
foreach ($priorLayerOutputVariablesGroups as $currentTeam) {
foreach ($currentTeam as $currentPlayer) {
$localCurrentPlayer = $currentPlayer->getKey();
$localCurrentPlayer = ($currentPlayer instanceof KeyedVariable) ? $currentPlayer->getKey() : new Player("");
$newRating = new Rating(
$currentPlayer->getValue()->getMean(),
$currentPlayer->getValue()->getStandardDeviation()

View File

@ -102,33 +102,33 @@ class TruncatedGaussianCorrectionFunctions
}
// the multiplicative correction of a double-sided truncated Gaussian with unit variance
public static function wWithinMarginScaled(float $teamPerformanceDifference, float $drawMargin, float $c): float
public static function wWithinMarginScaled(float $teamPerformanceDiff, float $drawMargin, float $c): float
{
return self::wWithinMargin($teamPerformanceDifference / $c, $drawMargin / $c);
return self::wWithinMargin($teamPerformanceDiff / $c, $drawMargin / $c);
}
// From F#:
public static function wWithinMargin(float $teamPerformanceDifference, float $drawMargin): float
public static function wWithinMargin(float $teamPerformanceDiff, float $drawMargin): float
{
$teamPerformanceDifferenceAbsoluteValue = abs($teamPerformanceDifference);
$denominator = GaussianDistribution::cumulativeTo($drawMargin - $teamPerformanceDifferenceAbsoluteValue)
$teamPerformanceDiffAbsValue = abs($teamPerformanceDiff);
$denominator = GaussianDistribution::cumulativeTo($drawMargin - $teamPerformanceDiffAbsValue)
-
GaussianDistribution::cumulativeTo(-$drawMargin - $teamPerformanceDifferenceAbsoluteValue);
GaussianDistribution::cumulativeTo(-$drawMargin - $teamPerformanceDiffAbsValue);
if ($denominator < 2.222758749e-162) {
return 1.0;
}
$vt = self::vWithinMargin($teamPerformanceDifferenceAbsoluteValue, $drawMargin);
$vt = self::vWithinMargin($teamPerformanceDiffAbsValue, $drawMargin);
return $vt * $vt +
(($drawMargin - $teamPerformanceDifferenceAbsoluteValue)
(($drawMargin - $teamPerformanceDiffAbsValue)
*
GaussianDistribution::at(
$drawMargin - $teamPerformanceDifferenceAbsoluteValue
$drawMargin - $teamPerformanceDiffAbsValue
)
- (-$drawMargin - $teamPerformanceDifferenceAbsoluteValue)
- (-$drawMargin - $teamPerformanceDiffAbsValue)
*
GaussianDistribution::at(-$drawMargin - $teamPerformanceDifferenceAbsoluteValue)) / $denominator;
GaussianDistribution::at(-$drawMargin - $teamPerformanceDiffAbsValue)) / $denominator;
}
}

View File

@ -13,9 +13,7 @@ use DNW\Skills\RankSorter;
use DNW\Skills\Rating;
use DNW\Skills\RatingContainer;
use DNW\Skills\SkillCalculator;
use DNW\Skills\SkillCalculatorSupportedOptions;
use DNW\Skills\TeamsRange;
use DNW\Skills\Team;
/**
* Calculates the new ratings for only two players.
@ -27,7 +25,7 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
{
public function __construct()
{
parent::__construct(SkillCalculatorSupportedOptions::NONE, TeamsRange::exactly(2), PlayersRange::exactly(1));
parent::__construct(SkillCalculator::NONE, TeamsRange::exactly(2), PlayersRange::exactly(1));
}
/**
@ -40,7 +38,6 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
): RatingContainer
{
// Basic argument checking
Guard::argumentNotNull($gameInfo, 'gameInfo');
$this->validateTeamCountAndPlayersCountPerTeam($teams);
// Make sure things are in order
@ -143,7 +140,6 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
*/
public function calculateMatchQuality(GameInfo $gameInfo, array $teams): float
{
Guard::argumentNotNull($gameInfo, 'gameInfo');
$this->validateTeamCountAndPlayersCountPerTeam($teams);
$team1 = $teams[0];

View File

@ -13,7 +13,6 @@ use DNW\Skills\RankSorter;
use DNW\Skills\Rating;
use DNW\Skills\RatingContainer;
use DNW\Skills\SkillCalculator;
use DNW\Skills\SkillCalculatorSupportedOptions;
use DNW\Skills\Team;
use DNW\Skills\TeamsRange;
@ -26,14 +25,14 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
{
public function __construct()
{
parent::__construct(SkillCalculatorSupportedOptions::NONE, TeamsRange::exactly(2), PlayersRange::atLeast(1));
parent::__construct(SkillCalculator::NONE, TeamsRange::exactly(2), PlayersRange::atLeast(1));
}
/**
* {@inheritdoc}
*/
public function calculateNewRatings(GameInfo $gameInfo, array $teams, array $teamRanks): RatingContainer
{
Guard::argumentNotNull($gameInfo, 'gameInfo');
$this->validateTeamCountAndPlayersCountPerTeam($teams);
RankSorter::sort($teams, $teamRanks);
@ -82,12 +81,12 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
$totalPlayers = $selfTeam->count() + $otherTeam->count();
$meanGetter = fn (Rating $currentRating): float => $currentRating->getMean();
$meanGetter = static fn(Rating $currentRating): float => $currentRating->getMean();
$selfMeanSum = BasicMath::sum($selfTeam->getAllRatings(), $meanGetter);
$otherTeamMeanSum = BasicMath::sum($otherTeam->getAllRatings(), $meanGetter);
$varianceGetter = fn (Rating $currentRating): float => BasicMath::square($currentRating->getStandardDeviation());
$varianceGetter = static fn(Rating $currentRating): float => BasicMath::square($currentRating->getStandardDeviation());
$c = sqrt(
BasicMath::sum($selfTeam->getAllRatings(), $varianceGetter)
@ -126,9 +125,9 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
}
$selfTeamAllPlayers = $selfTeam->getAllPlayers();
foreach ($selfTeamAllPlayers as $selfTeamCurrentPlayer) {
$localSelfTeamCurrentPlayer = $selfTeamCurrentPlayer;
$previousPlayerRating = $selfTeam->getRating($localSelfTeamCurrentPlayer);
foreach ($selfTeamAllPlayers as $selfTeamCurPlayer) {
$localSelfTeamCurPlayer = $selfTeamCurPlayer;
$previousPlayerRating = $selfTeam->getRating($localSelfTeamCurPlayer);
$meanMultiplier = (BasicMath::square($previousPlayerRating->getStandardDeviation()) + $tauSquared) / $c;
$stdDevMultiplier = (BasicMath::square($previousPlayerRating->getStandardDeviation()) + $tauSquared) / BasicMath::square($c);
@ -140,7 +139,7 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
(BasicMath::square($previousPlayerRating->getStandardDeviation()) + $tauSquared) * (1 - $w * $stdDevMultiplier)
);
$newPlayerRatings->setRating($localSelfTeamCurrentPlayer, new Rating($newMean, $newStdDev));
$newPlayerRatings->setRating($localSelfTeamCurPlayer, new Rating($newMean, $newStdDev));
}
}
@ -149,7 +148,6 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
*/
public function calculateMatchQuality(GameInfo $gameInfo, array $teams): float
{
Guard::argumentNotNull($gameInfo, 'gameInfo');
$this->validateTeamCountAndPlayersCountPerTeam($teams);
// We've verified that there's just two teams
@ -163,9 +161,9 @@ class TwoTeamTrueSkillCalculator extends SkillCalculator
$betaSquared = BasicMath::square($gameInfo->getBeta());
$meanGetter = fn (Rating $currentRating): float => $currentRating->getMean();
$meanGetter = static fn(Rating $currentRating): float => $currentRating->getMean();
$varianceGetter = fn (Rating $currentRating): float => BasicMath::square($currentRating->getStandardDeviation());
$varianceGetter = static fn(Rating $currentRating): float => BasicMath::square($currentRating->getStandardDeviation());
$team1MeanSum = BasicMath::sum($team1Ratings, $meanGetter);
$team1StdDevSquared = BasicMath::sum($team1Ratings, $varianceGetter);

View File

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace DNW\Skills\Tests\FactorGraphs;
use DNW\Skills\FactorGraphs\ScheduleStep;
use DNW\Skills\FactorGraphs\Factor;
use PHPUnit\Framework\TestCase;
class ScheduleStepTest extends TestCase
{
public function test(): void
{
$stub = $this->createStub(Factor::class);
$ss = new ScheduleStep('dummy', $stub, 0);
$this->assertEquals('dummy', (string)$ss);
}
}

View File

@ -7,19 +7,22 @@ namespace DNW\Skills\Tests\FactorGraphs;
use DNW\Skills\FactorGraphs\Variable;
use DNW\Skills\Numerics\GaussianDistribution;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(Variable::class)]
#[UsesClass(GaussianDistribution::class)]
class VariableTest extends TestCase
{
public function test(): void
public function testGetterSetter(): void
{
$gd_prior = new GaussianDistribution();
$var = new Variable('dummy', $gd_prior);
$var = new Variable($gd_prior);
$this->assertEquals($gd_prior, $var->getValue());
$gd_new = new GaussianDistribution();
$this->assertEquals($gd_new, $var->getValue());
$var->resetToPrior();
$this->assertEquals($gd_prior, $var->getValue());
$this->assertEquals('Variable[dummy]', (string)$var);
}
}

View File

@ -5,8 +5,13 @@ declare(strict_types=1);
namespace DNW\Skills\Tests;
use DNW\Skills\GameInfo;
use DNW\Skills\Rating;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(GameInfo::class)]
#[UsesClass(Rating::class)]
class GameInfoTest extends TestCase
{
public function testMembers(): void
@ -17,6 +22,6 @@ class GameInfoTest extends TestCase
$this->assertEquals(3, $gi->getBeta());
$this->assertEquals(4, $gi->getDynamicsFactor());
$this->assertEquals(5, $gi->getDrawProbability());
$this->assertInstanceOf(\DNW\Skills\Rating::class, $gi->getDefaultRating());
$this->assertInstanceOf(Rating::class, $gi->getDefaultRating());
}
}

View File

@ -7,34 +7,55 @@ namespace DNW\Skills\Tests;
use DNW\Skills\Guard;
use Exception;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(Guard::class)]
class GuardTest extends TestCase
{
public function testArgumentNotNull(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('dummy can not be null');
Guard::argumentNotNull(NULL, "dummy");
}
public function testargumentIsValidIndex(): void
public function testargumentIsValidIndexArgumentAbove(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('dummy is an invalid index');
Guard::argumentIsValidIndex(10, 10, "dummy");
}
public function testargumentIsValidIndex2(): void
public function testargumentIsValidIndexArgumentBelow(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('dummy is an invalid index');
Guard::argumentIsValidIndex(-1, 10, "dummy");
}
public function testargumentInRangeInclusive(): void
public function testargumentIsValidIndexArgumentValid(): void
{
Guard::argumentIsValidIndex(0, 10, "dummy");
Guard::argumentIsValidIndex(1, 10, "dummy");
Guard::argumentIsValidIndex(9, 10, "dummy");
$this->expectNotToPerformAssertions();
}
public function testargumentInRangeInclusiveAbove(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('dummy is not in the valid range [0, 100]');
Guard::argumentInRangeInclusive(101, 0, 100, "dummy");
}
public function testargumentInRangeInclusiveBelow(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage('dummy is not in the valid range [0, 100]');
Guard::argumentInRangeInclusive(-1, 0, 100, "dummy");
}
public function testargumentInRangeInclusiveValid(): void
{
Guard::argumentInRangeInclusive(0, 0, 100, "dummy");
Guard::argumentInRangeInclusive(1, 0, 100, "dummy");
Guard::argumentInRangeInclusive(50, 0, 100, "dummy");
Guard::argumentInRangeInclusive(99, 0, 100, "dummy");
Guard::argumentInRangeInclusive(100, 0, 100, "dummy");
$this->expectNotToPerformAssertions();
}
}

37
tests/HashMapTest.php Normal file
View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DNW\Skills\Tests;
use DNW\Skills\HashMap;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use stdClass;
#[CoversClass(HashMap::class)]
class HashMapTest extends TestCase
{
public function testHashmap(): void
{
$h = new HashMap();
$this->assertEquals([], $h->getAllKeys());
$this->assertEquals([], $h->getAllValues());
$o1 = new stdClass();
$o2 = new stdClass();
$h->setValue($o1, 1);
$h->setvalue($o2, 2);
$this->assertEquals([1, 2], $h->getAllValues());
$this->assertEquals([$o1, $o2], $h->getAllKeys());
$this->assertEquals(1, $h->getvalue($o1));
$this->assertEquals(2, $h->getvalue($o2));
$this->assertEquals(2, $h->count());
}
}

View File

@ -6,7 +6,9 @@ namespace DNW\Skills\Tests\Numerics;
use DNW\Skills\Numerics\BasicMath;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(BasicMath::class)]
class BasicMathTest extends TestCase
{
public function testSquare(): void
@ -20,12 +22,8 @@ class BasicMathTest extends TestCase
{
$arr = [1, 1, 1, 1];
$func_return = function (float $f): float {
return $f;
};
$func_double = function (float $f): float {
return $f * 2;
};
$func_return = static fn(float $f): float => $f;
$func_double = static fn(float $f): float => $f * 2;
$this->assertEquals(4, BasicMath::sum($arr, $func_return));
$this->assertEquals(8, BasicMath::sum($arr, $func_double));
}

View File

@ -7,7 +7,11 @@ namespace DNW\Skills\Tests\Numerics;
use DNW\Skills\Numerics\BasicMath;
use DNW\Skills\Numerics\GaussianDistribution;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(GaussianDistribution::class)]
#[UsesClass(BasicMath::class)]
class GaussianDistributionTest extends TestCase
{
private const ERROR_TOLERANCE = 0.000001;
@ -88,6 +92,21 @@ class GaussianDistributionTest extends TestCase
$m3s4 = new GaussianDistribution(3, 4);
$lpn2 = GaussianDistribution::logProductNormalization($m1s2, $m3s4);
$this->assertEqualsWithDelta(-2.5168046699816684, $lpn2, GaussianDistributionTest::ERROR_TOLERANCE);
$numerator = GaussianDistribution::fromPrecisionMean(1, 0);
$denominator = GaussianDistribution::fromPrecisionMean(1, 0);
$lrn = GaussianDistribution::logProductNormalization($numerator, $denominator);
$this->assertEquals(0, $lrn);
$numerator = GaussianDistribution::fromPrecisionMean(1, 1);
$denominator = GaussianDistribution::fromPrecisionMean(1, 0);
$lrn = GaussianDistribution::logProductNormalization($numerator, $denominator);
$this->assertEquals(0, $lrn);
$numerator = GaussianDistribution::fromPrecisionMean(1, 0);
$denominator = GaussianDistribution::fromPrecisionMean(1, 1);
$lrn = GaussianDistribution::logProductNormalization($numerator, $denominator);
$this->assertEquals(0, $lrn);
}
public function testLogRatioNormalization(): void
@ -97,6 +116,21 @@ class GaussianDistributionTest extends TestCase
$m3s4 = new GaussianDistribution(3, 4);
$lrn = GaussianDistribution::logRatioNormalization($m1s2, $m3s4);
$this->assertEqualsWithDelta(2.6157405972171204, $lrn, GaussianDistributionTest::ERROR_TOLERANCE);
$numerator = GaussianDistribution::fromPrecisionMean(1, 0);
$denominator = GaussianDistribution::fromPrecisionMean(1, 0);
$lrn = GaussianDistribution::logRatioNormalization($numerator, $denominator);
$this->assertEquals(0, $lrn);
$numerator = GaussianDistribution::fromPrecisionMean(1, 1);
$denominator = GaussianDistribution::fromPrecisionMean(1, 0);
$lrn = GaussianDistribution::logRatioNormalization($numerator, $denominator);
$this->assertEquals(0, $lrn);
$numerator = GaussianDistribution::fromPrecisionMean(1, 0);
$denominator = GaussianDistribution::fromPrecisionMean(1, 1);
$lrn = GaussianDistribution::logRatioNormalization($numerator, $denominator);
$this->assertEquals(0, $lrn);
}
public function testAbsoluteDifference(): void
@ -111,4 +145,27 @@ class GaussianDistributionTest extends TestCase
$absDiff2 = GaussianDistribution::absoluteDifference($m1s2, $m3s4);
$this->assertEqualsWithDelta(0.4330127018922193, $absDiff2, GaussianDistributionTest::ERROR_TOLERANCE);
}
public function testSubtract(): void
{
// Verified with Ralf Herbrich's F# implementation
$standardNormal = new GaussianDistribution(0, 1);
$absDiff = GaussianDistribution::subtract($standardNormal, $standardNormal);
$this->assertEqualsWithDelta(0.0, $absDiff, GaussianDistributionTest::ERROR_TOLERANCE);
$m1s2 = new GaussianDistribution(1, 2);
$m3s4 = new GaussianDistribution(3, 4);
$absDiff2 = GaussianDistribution::subtract($m1s2, $m3s4);
$this->assertEqualsWithDelta(0.4330127018922193, $absDiff2, GaussianDistributionTest::ERROR_TOLERANCE);
}
public function testfromPrecisionMean(): void
{
$gd = GaussianDistribution::fromPrecisionMean(0, 0);
$this->assertInfinite($gd->getVariance());
$this->assertInfinite($gd->getStandardDeviation());
$this->assertNan($gd->getMean());
$this->assertEquals(0, $gd->getPrecisionMean());
$this->assertEquals(0, $gd->getPrecision());
}
}

View File

@ -8,18 +8,97 @@ use DNW\Skills\Numerics\IdentityMatrix;
use DNW\Skills\Numerics\Matrix;
use DNW\Skills\Numerics\SquareMatrix;
use DNW\Skills\Numerics\DiagonalMatrix;
use DNW\Skills\Numerics\Vector;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use Exception;
#[CoversClass(Matrix::class)]
#[UsesClass(SquareMatrix::class)]
#[UsesClass(IdentityMatrix::class)]
#[UsesClass(DiagonalMatrix::class)]
#[CoversClass(SquareMatrix::class)]
#[CoversClass(IdentityMatrix::class)]
#[CoversClass(DiagonalMatrix::class)]
#[CoversClass(Vector::class)]
// phpcs:disable PSR2.Methods.FunctionCallSignature,Generic.Functions.FunctionCallArgumentSpacing.TooMuchSpaceAfterComma
class MatrixTest extends TestCase
{
public function testEmptyMatrix(): void
{
$m1 = new Matrix();
$this->assertEquals(0, $m1->getRowCount());
$this->assertEquals(0, $m1->getColumnCount());
$m2 = new Matrix(0, 0);
$this->assertEquals(0, $m2->getRowCount());
$this->assertEquals(0, $m2->getColumnCount());
$this->assertEquals(new Matrix(), Matrix::multiply($m1, $m2));
}
public function testIndexing(): void
{
$m = new Matrix(5, 5);
$m->setValue(0, 0, 1);
$this->assertEquals(1, $m->getValue(0, 0));
$m->setValue(0, 1, 2);
$this->assertEquals(2, $m->getValue(0, 1));
$m->setValue(1, 0, 3);
$this->assertEquals(3, $m->getValue(1, 0));
$m->setValue(1, 1, 4);
$this->assertEquals(4, $m->getValue(1, 1));
$m->setValue(3, 3, 11);
$this->assertEquals(11, $m->getValue(3, 3));
$m->setValue(4, 3, 22);
$this->assertEquals(22, $m->getValue(4, 3));
$m->setValue(3, 4, 33);
$this->assertEquals(33, $m->getValue(3, 4));
$m->setValue(4, 4, 44);
$this->assertEquals(44, $m->getValue(4, 4));
try {
$m->getValue(-1, -1);
$this->fail("No exception");
} catch (Exception $exception) {
$this->assertInstanceOf(Exception::class, $exception);
}
try {
$m->getValue(-1, 0);
$this->fail("No exception");
} catch (Exception $exception) {
$this->assertInstanceOf(Exception::class, $exception);
}
try {
$m->getValue(0, -1);
$this->fail("No exception");
} catch (Exception $exception) {
$this->assertInstanceOf(Exception::class, $exception);
}
try {
$m->getValue(5, 5);
$this->fail("No exception");
} catch (Exception $exception) {
$this->assertInstanceOf(Exception::class, $exception);
}
try {
$m->getValue(5, 4);
$this->fail("No exception");
} catch (Exception $exception) {
$this->assertInstanceOf(Exception::class, $exception);
}
try {
$m->getValue(4, 5);
$this->fail("No exception");
} catch (Exception $exception) {
$this->assertInstanceOf(Exception::class, $exception);
}
}
public function testOneByOneDeterminant(): void
{
$a = new SquareMatrix(1);
@ -264,5 +343,15 @@ class MatrixTest extends TestCase
$m2 = new Matrix(1, 1, [[1,1]]);
Matrix::multiply($m1, $m2);
}
public function testVector(): void
{
$vector = new Vector([1,2,3,4]);
$m1 = new Matrix(4, 1, [[1],[2],[3],[4]]);
$this->assertTrue($vector->equals($m1));
}
}
// phpcs:enable

View File

@ -6,20 +6,22 @@ namespace DNW\Skills\Tests\Numerics;
use DNW\Skills\Numerics\Range;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use Exception;
#[CoversClass(Range::class)]
class RangeTest extends TestCase
{
public function testConstructInvalidParam(): void
{
$this->expectException(Exception::class);
$range = new Range(10, 5);
new Range(10, 5);
}
public function testFactoryInclusiveInvalidParam(): void
{
$this->expectException(Exception::class);
$range = Range::inclusive(10, 5);
Range::inclusive(10, 5);
}
public function testNormalUse(): void

27
tests/PartialPlayTest.php Normal file
View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace DNW\Skills\Tests;
use DNW\Skills\PartialPlay;
use DNW\Skills\Player;
use DNW\Skills\Guard;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(PartialPlay::class)]
#[UsesClass(Player::class)]
#[UsesClass(Guard::class)]
class PartialPlayTest extends TestCase
{
public function testgetPartialPlayPercentage(): void
{
$p = new Player(1, 0.5);
$this->assertEquals($p->getPartialPlayPercentage(), PartialPlay::getPartialPlayPercentage($p));
$p = new Player(1, 0.000000);
$this->assertNotEquals(0.0, PartialPlay::getPartialPlayPercentage($p));
}
}

View File

@ -5,14 +5,19 @@ declare(strict_types=1);
namespace DNW\Skills\Tests;
use DNW\Skills\Player;
use DNW\Skills\Guard;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(Player::class)]
#[UsesClass(Guard::class)]
class PlayerTest extends TestCase
{
public function test(): void
public function testPlayerObjectGetterSetter(): void
{
$p = new Player('dummy', 0.1, 0.2);
$this->assertEquals('dummy', (string)$p);
$this->assertEquals('dummy', $p->getId());
$this->assertEquals(0.1, $p->getPartialPlayPercentage());
$this->assertEquals(0.2, $p->getPartialUpdatePercentage());
}

View File

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

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DNW\Skills\Tests;
use DNW\Skills\RatingContainer;
use DNW\Skills\HashMap;
use DNW\Skills\Player;
use DNW\Skills\Rating;
use DNW\Skills\Guard;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(RatingContainer::class)]
#[UsesClass(Hashmap::class)]
#[UsesClass(Player::class)]
#[UsesClass(Rating::class)]
#[UsesClass(Guard::class)]
class RatingContainerTest extends TestCase
{
public function testRatingContainer(): void
{
$rc = new RatingContainer();
$this->assertEquals([], $rc->getAllPlayers());
$this->assertEquals([], $rc->getAllRatings());
$this->assertEquals(0, $rc->count());
$p1 = new Player(1);
$p2 = new Player(2);
$r1 = new Rating(100, 10);
$r2 = new Rating(200, 20);
$rc->setRating($p1, $r1);
$rc->setRating($p2, $r2);
$this->assertEquals($r1, $rc->getRating($p1));
$this->assertEquals($r2, $rc->getRating($p2));
$this->assertEquals([$p1, $p2], $rc->getAllPlayers());
$this->assertEquals([$r1, $r2], $rc->getAllRatings());
$this->assertEquals(2, $rc->count());
}
}

View File

@ -5,8 +5,15 @@ declare(strict_types=1);
namespace DNW\Skills\Tests;
use DNW\Skills\Rating;
use DNW\Skills\Numerics\BasicMath;
use DNW\Skills\Numerics\GaussianDistribution;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(Rating::class)]
#[UsesClass(BasicMath::class)]
#[UsesClass(GaussianDistribution::class)]
class RatingTest extends TestCase
{
public function testGetRatingParameters(): void
@ -15,7 +22,6 @@ class RatingTest extends TestCase
$this->assertEquals(100, $rating->getMean());
$this->assertEquals(10, $rating->getStandardDeviation());
$this->assertEquals(50, $rating->getConservativeRating());
$this->assertEquals("mean=100.0000, standardDeviation=10.0000", (string)$rating);
}
public function testPartialUpdate(): void
@ -26,10 +32,8 @@ class RatingTest extends TestCase
$rating_partial = $rating->getPartialUpdate($ratingOld, $ratingNew, 0.5);
$this->assertEquals(150, $rating_partial->getMean());
$this->assertEquals(10, $rating_partial->getStandardDeviation());
$this->assertEquals(100, $rating_partial->getConservativeRating());
$this->assertEquals("mean=150.0000, standardDeviation=10.0000", (string)$rating_partial);
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace DNW\Skills\Tests;
use DNW\Skills\SkillCalculator;
use DNW\Skills\TeamsRange;
use DNW\Skills\PlayersRange;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\Attributes\RequiresPhpunit;
#[CoversClass(SkillCalculator::class)]
#[UsesClass(\DNW\Skills\Numerics\Range::class)]
#[UsesClass(PlayersRange::class)]
#[UsesClass(TeamsRange::class)]
#[RequiresPhpunit('<12.0')]
class SkillCalculatorTest extends TestCase
{
public function testisSupported(): void
{
$calculator = $this->getMockForAbstractClass(SkillCalculator::class, [SkillCalculator::PARTIAL_PLAY, new TeamsRange(1, 2), new PlayersRange(1, 2)]);
$this->assertEquals(TRUE, $calculator->isSupported(SkillCalculator::PARTIAL_PLAY));
$this->assertEquals(FALSE, $calculator->isSupported(SkillCalculator::PARTIAL_UPDATE));
}
}

69
tests/TeamTest.php Normal file
View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace DNW\Skills\Tests;
use DNW\Skills\Team;
use DNW\Skills\RatingContainer;
use DNW\Skills\HashMap;
use DNW\Skills\Player;
use DNW\Skills\Rating;
use DNW\Skills\Guard;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(Team::class)]
#[CoversClass(RatingContainer::class)]
#[UsesClass(Hashmap::class)]
#[UsesClass(Player::class)]
#[UsesClass(Rating::class)]
#[UsesClass(Guard::class)]
class TeamTest extends TestCase
{
public function testTeam(): void
{
$p1 = new Player(1);
$p2 = new Player(2);
$r1 = new Rating(100, 10);
$r2 = new Rating(200, 20);
$rc = new Team($p1, $r1);
$this->assertEquals($r1, $rc->getRating($p1));
$this->assertEquals([$p1], $rc->getAllPlayers());
$this->assertEquals([$r1], $rc->getAllRatings());
$this->assertEquals(1, $rc->count());
$rc->addPlayer($p2, $r2);
$this->assertEquals($r2, $rc->getRating($p2));
$this->assertEquals([$p1, $p2], $rc->getAllPlayers());
$this->assertEquals([$r1, $r2], $rc->getAllRatings());
$this->assertEquals(2, $rc->count());
}
public function testTeamConstructor(): void
{
$p = new Player(0);
$r = new Rating(100, 10);
$rc = new Team(NULL, NULL);
$this->assertEquals(0, $rc->count());
$rc = new Team($p, NULL);
$this->assertEquals(0, $rc->count());
$rc = new Team(NULL, $r);
$this->assertEquals(0, $rc->count());
$rc = new Team($p, $r);
$this->assertEquals($r, $rc->getRating($p));
$this->assertEquals([$p], $rc->getAllPlayers());
$this->assertEquals([$r], $rc->getAllRatings());
$this->assertEquals(1, $rc->count());
}
}

View File

@ -5,8 +5,15 @@ declare(strict_types=1);
namespace DNW\Skills\Tests\TrueSkill;
use DNW\Skills\TrueSkill\DrawMargin;
use DNW\Skills\Numerics\BasicMath;
use DNW\Skills\Numerics\GaussianDistribution;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(DrawMargin::class)]
#[UsesClass(BasicMath::class)]
#[UsesClass(GaussianDistribution::class)]
class DrawMarginTest extends TestCase
{
private const ERROR_TOLERANCE = 0.000001;
@ -23,6 +30,6 @@ class DrawMarginTest extends TestCase
private function assertDrawMargin(float $drawProbability, float $beta, float $expected): void
{
$actual = DrawMargin::getDrawMarginFromDrawProbability($drawProbability, $beta);
$this->assertEqualsWithDelta($expected, $actual, DrawMarginTest::ERROR_TOLERANCE);
$this->assertEqualsWithDelta($expected, $actual, self::ERROR_TOLERANCE);
}
}

View File

@ -4,11 +4,11 @@ declare(strict_types=1);
namespace DNW\Skills\Tests\TrueSkill;
use DNW\Skills\SkillCalculator;
use DNW\Skills\GameInfo;
use DNW\Skills\Player;
use DNW\Skills\Team;
use DNW\Skills\TrueSkill\FactorGraphTrueSkillCalculator;
use DNW\Skills\SkillCalculatorSupportedOptions;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversNothing;
@ -78,6 +78,6 @@ class FactorGraphTrueSkillCalculatorTest extends TestCase
public function testMethodisSupported(): void
{
$calculator = new FactorGraphTrueSkillCalculator();
$this->assertEquals(TRUE, $calculator->isSupported(SkillCalculatorSupportedOptions::PARTIAL_PLAY));
$this->assertEquals(TRUE, $calculator->isSupported(SkillCalculator::PARTIAL_PLAY));
}
}

View File

@ -9,7 +9,6 @@ use DNW\Skills\Player;
use DNW\Skills\Rating;
use DNW\Skills\SkillCalculator;
use DNW\Skills\Team;
use DNW\Skills\Teams;
use PHPUnit\Framework\TestCase;
class TrueSkillCalculatorTests
@ -70,7 +69,6 @@ class TrueSkillCalculatorTests
// online calculator at http://atom.research.microsoft.com/trueskill/rankcalculator.aspx
//
// All match quality expected values came from the online calculator
// In both cases, there may be some discrepancy after the first decimal point. I think this is due to my implementation
// using slightly higher precision in GaussianDistribution.
@ -86,7 +84,7 @@ class TrueSkillCalculatorTests
$team1 = new Team($player1, $gameInfo->getDefaultRating());
$team2 = new Team($player2, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
@ -109,7 +107,7 @@ class TrueSkillCalculatorTests
$team1 = new Team($player1, $gameInfo->getDefaultRating());
$team2 = new Team($player2, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 1]);
@ -132,7 +130,7 @@ class TrueSkillCalculatorTests
$team1 = new Team($player1, new Rating(1301.0007, 42.9232));
$team2 = new Team($player2, new Rating(1188.7560, 42.5570));
$newRatings = $calculator->calculateNewRatings($gameInfo, Teams::concat($team1, $team2), [1, 2]);
$newRatings = $calculator->calculateNewRatings($gameInfo, [$team1, $team2], [1, 2]);
$player1NewRating = $newRatings->getRating($player1);
self::assertRating($testClass, 1304.7820836053318, 42.843513887848658, $player1NewRating);
@ -153,7 +151,7 @@ class TrueSkillCalculatorTests
$team2 = new Team($player2, new Rating(50, 12.5));
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 1]);
@ -184,7 +182,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player2, $gameInfo->getDefaultRating());
$team2->addPlayer($player3, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
// Winners
@ -219,7 +217,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player3, $gameInfo->getDefaultRating());
$team2->addPlayer($player4, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
// Winners
@ -249,7 +247,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player2, new Rating(20, 7));
$team2->addPlayer($player3, new Rating(25, 8));
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
// Winners
@ -280,7 +278,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player3, $gameInfo->getDefaultRating());
$team2->addPlayer($player4, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
// Winners
@ -310,7 +308,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player2, $gameInfo->getDefaultRating());
$team2->addPlayer($player3, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 1]);
// Winners
@ -341,7 +339,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player3, $gameInfo->getDefaultRating());
$team2->addPlayer($player4, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 1]);
// Winners
@ -381,7 +379,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player7, $gameInfo->getDefaultRating());
$team2->addPlayer($player8, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
// Winners
@ -419,7 +417,7 @@ class TrueSkillCalculatorTests
$gameInfo = new GameInfo();
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLoseExpected = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
// Winners
@ -431,7 +429,7 @@ class TrueSkillCalculatorTests
self::assertRating($testClass, 29.785, 3.958, $newRatingsWinLoseExpected->getRating($player4));
self::assertRating($testClass, 30.879, 2.983, $newRatingsWinLoseExpected->getRating($player5));
$newRatingsWinLoseUpset = $calculator->calculateNewRatings($gameInfo, Teams::concat($team1, $team2), [2, 1]);
$newRatingsWinLoseUpset = $calculator->calculateNewRatings($gameInfo, [$team1, $team2], [2, 1]);
// Winners
self::assertRating($testClass, 32.012, 3.877, $newRatingsWinLoseUpset->getRating($player4));
@ -463,7 +461,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player3, new Rating(25, 4));
$team2->addPlayer($player4, new Rating(30, 3));
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 1]);
// Winners
@ -495,7 +493,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player3, new Rating(35, 7));
$team2->addPlayer($player4, new Rating(40, 5));
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
// Winners
@ -535,7 +533,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player7, $gameInfo->getDefaultRating());
$team2->addPlayer($player8, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
@ -572,7 +570,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($player3, $gameInfo->getDefaultRating());
$team2->addPlayer($player4, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 1]);
// Winners
@ -598,7 +596,7 @@ class TrueSkillCalculatorTests
$team2 = new Team($player2, $gameInfo->getDefaultRating());
$team3 = new Team($player3, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2, $team3);
$teams = [$team1, $team2, $team3];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2, 3]);
$player1NewRating = $newRatings->getRating($player1);
@ -625,7 +623,7 @@ class TrueSkillCalculatorTests
$team2 = new Team($player2, $gameInfo->getDefaultRating());
$team3 = new Team($player3, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2, $team3);
$teams = [$team1, $team2, $team3];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 1, 1]);
$player1NewRating = $newRatings->getRating($player1);
@ -653,7 +651,7 @@ class TrueSkillCalculatorTests
$team3 = new Team($player3, $gameInfo->getDefaultRating());
$team4 = new Team($player4, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2, $team3, $team4);
$teams = [$team1, $team2, $team3, $team4];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2, 3, 4]);
@ -687,7 +685,7 @@ class TrueSkillCalculatorTests
$team4 = new Team($player4, $gameInfo->getDefaultRating());
$team5 = new Team($player5, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2, $team3, $team4, $team5);
$teams = [$team1, $team2, $team3, $team4, $team5];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2, 3, 4, 5]);
$player1NewRating = $newRatings->getRating($player1);
@ -729,7 +727,7 @@ class TrueSkillCalculatorTests
$team7 = new Team($player7, $gameInfo->getDefaultRating());
$team8 = new Team($player8, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2, $team3, $team4, $team5, $team6, $team7, $team8);
$teams = [$team1, $team2, $team3, $team4, $team5, $team6, $team7, $team8];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 1, 1, 1, 1, 1, 1, 1]);
$player1NewRating = $newRatings->getRating($player1);
@ -781,7 +779,7 @@ class TrueSkillCalculatorTests
$team7 = new Team($player7, new Rating(40, 2));
$team8 = new Team($player8, new Rating(45, 1));
$teams = Teams::concat($team1, $team2, $team3, $team4, $team5, $team6, $team7, $team8);
$teams = [$team1, $team2, $team3, $team4, $team5, $team6, $team7, $team8];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2, 3, 4, 5, 6, 7, 8]);
$player1NewRating = $newRatings->getRating($player1);
@ -849,24 +847,7 @@ class TrueSkillCalculatorTests
$team15 = new Team($player15, $gameInfo->getDefaultRating());
$team16 = new Team($player16, $gameInfo->getDefaultRating());
$teams = Teams::concat(
$team1,
$team2,
$team3,
$team4,
$team5,
$team6,
$team7,
$team8,
$team9,
$team10,
$team11,
$team12,
$team13,
$team14,
$team15,
$team16
);
$teams = [$team1, $team2, $team3, $team4, $team5, $team6, $team7, $team8, $team9, $team10, $team11, $team12, $team13, $team14, $team15, $team16];
$newRatings = $calculator->calculateNewRatings(
$gameInfo,
@ -952,7 +933,7 @@ class TrueSkillCalculatorTests
$team3->addPlayer($player7, new Rating(50, 5));
$team3->addPlayer($player8, new Rating(30, 2));
$teams = Teams::concat($team1, $team2, $team3);
$teams = [$team1, $team2, $team3];
$newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2, 2]);
// Winners
@ -986,7 +967,7 @@ class TrueSkillCalculatorTests
$team2->addPlayer($p2, $gameInfo->getDefaultRating());
$team2->addPlayer($p3, $gameInfo->getDefaultRating());
$teams = Teams::concat($team1, $team2);
$teams = [$team1, $team2];
$newRatings = $calculator->calculateNewRatings($gameInfo, $teams, [1, 2]);
$p1NewRating = $newRatings->getRating($p1);

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DNW\Skills\Tests\TrueSkill;
use DNW\Skills\TrueSkill\TruncatedGaussianCorrectionFunctions;
use DNW\Skills\Numerics\BasicMath;
use DNW\Skills\Numerics\GaussianDistribution;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
#[CoversClass(TruncatedGaussianCorrectionFunctions::class)]
#[UsesClass(BasicMath::class)]
#[UsesClass(GaussianDistribution::class)]
class TruncatedGaussianCorrectionFunctionsTest extends TestCase
{
public function testvGreaterThan(): void
{
// Test values taken from Ralf Herbrich's F# TrueSkill implementation
$want = 0.4181660649773850;
$tVar = 0.7495591915280050;
$eps = 0.0631282276750071;
$this->assertEqualsWithDelta($want, TruncatedGaussianCorrectionFunctions::vExceedsMargin($tVar, $eps), 1e-6);
}
public function testwGreaterThan(): void
{
// Test values taken from Ralf Herbrich's F# TrueSkill implementation
$want = 0.4619049929317120;
$tVar = 0.7495591915280050;
$eps = 0.0631282276750071;
$this->assertEqualsWithDelta($want, TruncatedGaussianCorrectionFunctions::wExceedsMargin($tVar, $eps), 1e-6);
}
public function testvWithin(): void
{
// Test values taken from Ralf Herbrich's F# TrueSkill implementation
$want = -0.7485644072749330;
$tVar = 0.7495591915280050;
$eps = 0.0631282276750071;
$this->assertEqualsWithDelta($want, TruncatedGaussianCorrectionFunctions::vWithinMargin($tVar, $eps), 1e-6);
}
public function testwWithin(): void
{
// Test values taken from Ralf Herbrich's F# TrueSkill implementation
$want = 0.9986734210033660;
$tVar = 0.7495591915280050;
$eps = 0.0631282276750071;
$this->assertEqualsWithDelta($want, TruncatedGaussianCorrectionFunctions::wWithinMargin($tVar, $eps), 1e-6);
}
}

View File

@ -17,7 +17,6 @@ class TwoPlayerTrueSkillCalculatorTest extends TestCase
{
$calculator = new TwoPlayerTrueSkillCalculator();
// We only support two players
TrueSkillCalculatorTests::testAllTwoPlayerScenarios($this, $calculator);
}
}

View File

@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\CoversNothing;
//#[CoversClass(TwoTeamTrueSkillCalculator::class)]
#[CoversClass(TwoTeamTrueSkillCalculator::class)]
class TwoTeamTrueSkillCalculatorTest extends TestCase
{
#[CoversNothing]
@ -17,7 +17,6 @@ class TwoTeamTrueSkillCalculatorTest extends TestCase
{
$calculator = new TwoTeamTrueSkillCalculator();
// We only support two players
TrueSkillCalculatorTests::testAllTwoPlayerScenarios($this, $calculator);
TrueSkillCalculatorTests::testAllTwoTeamScenarios($this, $calculator);
}