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