294 lines
9.9 KiB
PHP
294 lines
9.9 KiB
PHP
<?php
|
|
|
|
require_once "vendor/autoload.php";
|
|
|
|
require_once "AutoStoring.php";
|
|
|
|
require_once "ExpressionBuilder.php";
|
|
|
|
require_once "globals.php";
|
|
|
|
require_once "Test.php";
|
|
|
|
class TestMgr
|
|
{
|
|
private \SleekDB\Store $db; // test database
|
|
|
|
// -------------
|
|
|
|
// Update timed tests.
|
|
// function updateTimedTests(array $test_data_array)
|
|
// {
|
|
// $now = time();
|
|
// foreach ($test_data_array as $test_data) {
|
|
// // look for unprocessed expired tests
|
|
// if (($test_data["state"] === TEST_ONGOING) && ($test_data["time_limited"]) && ($test_data["end_limit_time"] < $now)) {
|
|
// conclude_test($test_data["_id"]);
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// -------------
|
|
|
|
function __construct()
|
|
{
|
|
$this->db = new \SleekDB\Store(TESTDB, DATADIR, ["timeout" => false]);
|
|
}
|
|
|
|
// Get test by ID from the database.
|
|
function getTest(string $testid): Test|null
|
|
{
|
|
$test_data_array = $this->db->findById($testid);
|
|
return count($test_data_array) != 0 ? new Test($this, $test_data_array) : null;
|
|
}
|
|
|
|
// Update test in the database.
|
|
function updateTest(Test &$test): void
|
|
{
|
|
$a = $test->toArray();
|
|
$this->db->update($a);
|
|
}
|
|
|
|
// Add test to the database.
|
|
function addTest(Game &$game, User &$user): Test
|
|
{
|
|
// create new test
|
|
$test = new Test($this, $game, $user);
|
|
|
|
// insert into database
|
|
$a = $test->toArray(["_id"]);
|
|
$a = $this->db->insert($a);
|
|
|
|
$test = new Test($this, $a);
|
|
return $test;
|
|
}
|
|
|
|
function addOrContinueTest(Game &$game, User &$user): Test|null
|
|
{
|
|
// check if the user had taken this test before
|
|
$fetch_criteria = [["gameid", "=", (int)$game->getId()], "AND", ["nickname", "=", $user->getNickname()]];
|
|
$previous_tests = $this->db->findBy($fetch_criteria);
|
|
if (count($previous_tests) > 0) { // if there are previous attempts, then...
|
|
fetch:
|
|
// re-fetch tests, look only for ongoing
|
|
$ongoing_tests = $this->db->findBy([$fetch_criteria, "AND", ["state", "=", Test::TEST_ONGOING]]);
|
|
if (count($ongoing_tests) !== 0) { // if there's an ongoing test
|
|
$testid = $ongoing_tests[0]["_id"];
|
|
$test = $this->getTest($testid);
|
|
if ($test->isConcluded()) { // tests get concluded if they got found to be expired
|
|
goto fetch; // like a loop...
|
|
}
|
|
return $test;
|
|
} else { // there's no ongoing test
|
|
if ($game->getProperties()["repeatable"]) { // test is repeatable...
|
|
goto add_test;
|
|
} else { // test is non-repeatable, cannot be attempted more times
|
|
return null;
|
|
}
|
|
}
|
|
} else { // there were no previous attempts
|
|
goto add_test;
|
|
}
|
|
|
|
// ----------------
|
|
|
|
add_test:
|
|
return $this->addTest($game, $user);
|
|
|
|
}
|
|
|
|
// Delete test from the database.
|
|
function deleteTest(string $testid): void
|
|
{
|
|
$this->db->deleteById($testid);
|
|
}
|
|
|
|
// Get concluded tests by game ID and nickname.
|
|
function getConcludedTests(string $gameid, string $nickname): array
|
|
{
|
|
$fetch_criteria = [["gameid", "=", (int)$gameid], "AND", ["nickname", "=", $nickname], "AND", ["state", "=", Test::TEST_CONCLUDED]];
|
|
$test_data_array = $this->db->findBy($fetch_criteria);
|
|
$tests = [];
|
|
foreach ($test_data_array as $a) {
|
|
$tests[] = new Test($this, $a);
|
|
}
|
|
return $tests;
|
|
}
|
|
|
|
// Get test results by game ID.
|
|
function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_task_data, bool $best_ones_only, array ...$furtherFilters): array
|
|
{
|
|
$qb = $this->db->createQueryBuilder();
|
|
$qb = $qb->where(["gameid", "=", (int)$gameid]);
|
|
|
|
// filtering
|
|
if (trim($filter) !== "") {
|
|
|
|
// auto complete starting and ending parenthesis
|
|
if (!str_starts_with($filter, "(")) {
|
|
$filter = "(" . $filter;
|
|
}
|
|
if (!str_ends_with($filter, ")")) {
|
|
$filter = $filter . ")";
|
|
}
|
|
|
|
$criteria = ExpressionBuilder::buildQuery($filter);
|
|
|
|
$qb->where($criteria);
|
|
}
|
|
|
|
// add further filters
|
|
if (count($furtherFilters) > 0) {
|
|
foreach ($furtherFilters as $ff) {
|
|
if ($ff !== []) {
|
|
$qb->where($ff);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ordering
|
|
if (trim($orderby) !== "") {
|
|
$ordering = ExpressionBuilder::buildOrdering($orderby);
|
|
$qb->orderBy($ordering);
|
|
}
|
|
|
|
// excluding task data
|
|
if ($exclude_task_data) {
|
|
$qb->except(["challenges"]);
|
|
}
|
|
|
|
$test_data_array = $qb->getQuery()->fetch();
|
|
|
|
// if only the best results should be included, then...
|
|
if ($best_ones_only) {
|
|
// filter out ongoing ones
|
|
$tests = array_filter($test_data_array, fn($test) => $test["state"] === Test::TEST_CONCLUDED);
|
|
|
|
// sort by result
|
|
usort($tests, fn($a, $b) => $a["summary"]["percentage"] > $b["summary"]["percentage"]);
|
|
|
|
// gather best tests by username here
|
|
$best_test_ids = [];
|
|
foreach ($tests as $test) {
|
|
$nickname = $test["nickname"];
|
|
if (!in_array($nickname, $best_test_ids)) {
|
|
$best_test_ids[$nickname] = $test["_id"];
|
|
}
|
|
}
|
|
|
|
// just keep values, drop the keys (nicknames)
|
|
$best_test_ids = array_values($best_test_ids);
|
|
|
|
// remove non-best results
|
|
$test_data_array = array_filter($test_data_array, fn($test) => in_array($test["_id"], $best_test_ids));
|
|
|
|
// renumber results
|
|
$test_data_array = array_values($test_data_array);
|
|
}
|
|
|
|
return $test_data_array;
|
|
}
|
|
|
|
// Generate detailed statistics.
|
|
function generateDetailedStats(string $gameid, array $testids): array
|
|
{
|
|
if ((count($testids) === 0) || ($gameid === "")) {
|
|
return [];
|
|
}
|
|
|
|
|
|
// fetch relevant entries
|
|
$qb = $this->db->createQueryBuilder();
|
|
$criteria = [["gameid", "=", (int)$gameid], "AND", ["state", "=", "concluded"], "AND", ["_id", "IN", $testids]];
|
|
$qb->where($criteria);
|
|
$qb->select(["challenges"]);
|
|
$entries = $qb->getQuery()->fetch();
|
|
|
|
$task_indices = [];
|
|
|
|
// count answers
|
|
$aggregated = [];
|
|
foreach ($entries as $entry) {
|
|
foreach ($entry["challenges"] as $task) {
|
|
$correct_answer = $task["answers"][$task["correct_answer"]];
|
|
$compound = $task["question"] . $correct_answer . count($task["answers"]) . $task["image_url"];
|
|
$idhash = md5($compound);
|
|
|
|
// if this is a new task to the list...
|
|
if (!isset($task_indices[$idhash])) {
|
|
$task_indices[$idhash] = count($task_indices);
|
|
$task_info = [ // copy challenge info
|
|
"hash" => $idhash,
|
|
"image_url" => $task["image_url"],
|
|
"question" => $task["question"],
|
|
"answers" => $task["answers"],
|
|
"correct_answer" => $correct_answer,
|
|
"player_answers" => array_fill(0, count($task["answers"]), 0),
|
|
"answer_count" => count($task["answers"]),
|
|
"skipped" => 0
|
|
];
|
|
$aggregated[$task_indices[$idhash]] = $task_info; // insert task info
|
|
}
|
|
|
|
// fetch task index
|
|
$task_idx = $task_indices[$idhash];
|
|
|
|
// add up player answer
|
|
$player_answer = trim($task["player_answer"]);
|
|
if (($player_answer !== "") && ($player_answer != -1)) { // player answered
|
|
$answer_idx = array_search($task["answers"][$task["player_answer"]], $aggregated[$task_idx]["answers"]); // transform player answer index to report answer index
|
|
$aggregated[$task_idx]["player_answers"][(int)$answer_idx]++;
|
|
} else { // player has not answered or provided an unprocessable answer
|
|
$aggregated[$task_idx]["skipped"]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// produce derived info
|
|
foreach ($aggregated as &$entry) {
|
|
$entry["answer_ratio"] = $entry["player_answers"];
|
|
$answer_n = count($entry["answer_ratio"]);
|
|
$sum = array_sum($entry["player_answers"]);
|
|
|
|
if ($sum === 0) {
|
|
continue;
|
|
}
|
|
|
|
for ($i = 0; $i < $answer_n; $i++) {
|
|
$entry["answer_ratio"][$i] = $entry["player_answers"][$i] / $sum;
|
|
}
|
|
}
|
|
|
|
// match tasks
|
|
return $aggregated;
|
|
}
|
|
|
|
// Upgrade test. Just load tests and save them.
|
|
function upgradeTests(array $ids = []): void
|
|
{
|
|
$a = [];
|
|
if ($ids === []) {
|
|
$a = $this->db->findAll();
|
|
} else {
|
|
$a = $this->db->findBy(["_id", "IN", $ids]);
|
|
}
|
|
|
|
foreach ($a as $t) {
|
|
$test = new Test($this, $t);
|
|
$test->storeMods();
|
|
}
|
|
}
|
|
|
|
// Extract timed test IDs. Scan the database for tests with time limit ON.
|
|
function extractExpiredTimedTestIds(bool $ongoingOnly = true): array
|
|
{
|
|
$query = [["time_limited", "=", true], "AND", ["end_limit_time", "<", time()]];
|
|
if ($ongoingOnly) {
|
|
$query = [...$query, "AND", ["state", "=", Test::TEST_ONGOING]];
|
|
}
|
|
|
|
$qb = $this->db->createQueryBuilder();
|
|
$a = $qb->where($query)->select(["_id"])->orderBy(["_id" => "ASC"])->getQuery()->fetch();
|
|
return array_map(fn($a) => $a["_id"], $a);
|
|
}
|
|
} |