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