- Challenge modularization initiated

This commit is contained in:
Wiesner András 2025-09-25 09:52:33 +02:00
parent c1ba2ec74a
commit 7c533f91f0
7 changed files with 293 additions and 45 deletions

View File

@ -38,7 +38,7 @@ class Game extends AutoStoring
// ------- // -------
static private function patchUpGameDate(array &$a) : void static private function patchUpGameData(array &$a) : void
{ {
$version = $a["version"] ?? 0; $version = $a["version"] ?? 0;
if ($version < 2) { // update to game version 2 if ($version < 2) { // update to game version 2
@ -49,6 +49,13 @@ class Game extends AutoStoring
$a["version"] = 2; $a["version"] = 2;
} }
if ($version < 3) {
return;
//$a["version"] = 3;
}
} }
// Store modifications. // Store modifications.
@ -106,7 +113,7 @@ class Game extends AutoStoring
static function fromArray(GameMgr &$gameMgr, array $a): Game static function fromArray(GameMgr &$gameMgr, array $a): Game
{ {
$id = $a["_id"] ?? -1; $id = $a["_id"] ?? -1;
self::patchUpGameDate($a); self::patchUpGameData($a);
return new Game($gameMgr, $a["name"], $a["description"], $id, $a["owner"], $a["contributors"], return new Game($gameMgr, $a["name"], $a["description"], $id, $a["owner"], $a["contributors"],
$a["game_file_present"], $a["properties"], $a["public"], $a["public_id"], $a["version"]); $a["game_file_present"], $a["properties"], $a["public"], $a["public_id"], $a["version"]);
} }
@ -136,7 +143,7 @@ class Game extends AutoStoring
return $a; return $a;
} }
// Export challenges to a CSV file. // Export challenges to a CSV file. TODO: ez csak a feleletválasztóshoz lesz jó
function exportChallengesToCSV(&$f): void function exportChallengesToCSV(&$f): void
{ {
// load challenges // load challenges
@ -185,7 +192,7 @@ class Game extends AutoStoring
const CSV_ENCODINGS = ["UTF-8", "Windows-1252"]; const CSV_ENCODINGS = ["UTF-8", "Windows-1252"];
// Import challenges from a CSV table. // Import challenges from a CSV table. TODO: ez csak a feleletválasztós betöltésére lesz jó
function importChallengesFromCSV(string $csv_path): array function importChallengesFromCSV(string $csv_path): array
{ {
// convert text encoding into UTF-8 // convert text encoding into UTF-8

View File

@ -79,7 +79,7 @@ class Answer
} }
} }
class Challenge class ChallengeReport
{ {
private string $question; private string $question;
private array $answers; private array $answers;
@ -152,7 +152,7 @@ class ReportSection
function __construct(string $title, array $challenges) function __construct(string $title, array $challenges)
{ {
$this->title = $title; $this->title = $title;
$this->challenges = array_map(fn($ch) => new Challenge($ch), $challenges); $this->challenges = array_map(fn($ch) => new ChallengeReport($ch), $challenges);
} }
function getChallenges(): array function getChallenges(): array

View File

@ -13,15 +13,15 @@ const TEST_CONCLUDED = "concluded";
class TestSummary class TestSummary
{ {
public int $challengeN; // Number of challenges public int $maxMark; // Number of challenges
public int $correctAnswerN; // Number of correct answers public int $mark; // Number of correct answers
private float $percentage; // Ratio of correct answers private float $percentage; // Ratio of correct answers
// Calculate percentage. // Calculate percentage.
private function calculatePercentage(): void private function calculatePercentage(): void
{ {
if ($this->challengeN > 0) { if ($this->maxMark > 0) {
$this->percentage = $this->correctAnswerN / (double)$this->challengeN * 100.0; $this->percentage = $this->mark / (double)$this->maxMark * 100.0;
} else { // avoid division by zero } else { // avoid division by zero
$this->percentage = 0.0; $this->percentage = 0.0;
} }
@ -29,45 +29,266 @@ class TestSummary
function __construct(int $challengeN, int $correctAnswerN) function __construct(int $challengeN, int $correctAnswerN)
{ {
$this->challengeN = $challengeN; $this->maxMark = $challengeN;
$this->correctAnswerN = $correctAnswerN; $this->mark = $correctAnswerN;
$this->calculatePercentage(); $this->calculatePercentage();
} }
// Get challenge count. // Get challenge count.
function getChallengeN(): int function getMaxMark(): int
{ {
return $this->challengeN; return $this->maxMark;
} }
// Get number of correct answers. // Get number of correct answers.
function getCorrectAnswerN(): int function getMark(): int
{ {
return $this->correctAnswerN; return $this->mark;
} }
function setCorrectAnswerN(int $correctAnswerN): void function setMark(int $mark): void
{ {
$this->correctAnswerN = $correctAnswerN; $this->mark = $mark;
$this->calculatePercentage(); $this->calculatePercentage();
} }
// Get ratio of correct results. // Get ratio of correct results.
function getPercentage(): float function getPercentage(): float
{ {
return ($this->correctAnswerN * 100.0) / $this->challengeN; return ($this->mark * 100.0) / $this->maxMark;
} }
// Build from array. // Build from array.
static function fromArray(array $a): TestSummary static function fromArray(array $a): TestSummary
{ {
return new TestSummary($a["challenge_n"], $a["correct_answer_n"]); if (!isset($a["max_mark"]) || !isset($a["mark"])) { // backward compatibility
return new TestSummary($a["challenge_n"], $a["correct_answer_n"]);
} else {
return new TestSummary($a["max_mark"], $a["mark"]);
}
} }
// Convert to array. // Convert to array.
function toArray(): array function toArray(): array
{ {
return ["challenge_n" => $this->challengeN, "correct_answer_n" => $this->correctAnswerN, "percentage" => $this->percentage]; return ["challenge_n" => $this->maxMark, "correct_answer_n" => $this->mark, "percentage" => $this->percentage];
}
}
class Challenge
{
protected string $type; // challenge type
protected float $max_mark; // maximum points that can be collected at this challenge
protected bool $is_template; // this challenge is a template
function __construct(string $type)
{
$this->type = $type;
$this->is_template = false;
$this->max_mark = 1.0;
}
// save answer
function saveAnswer(int|string $ans): bool
{
return false;
}
// clear answer
function clearAnswer(int|string $ans): bool
{
return false;
}
// get challenge type
function getType(): string
{
return $this->type;
}
function setMaxMark(float $max_mark): void
{
$this->max_mark = $max_mark;
}
function getMaxMark(): float {
return $this->max_mark;
}
function getMark(): float
{
return 1.0;
}
function toArray(): array
{
return ["type" => $this->type];
}
function setTemplate(bool $is_template): void
{
$this->is_template = $is_template;
}
function isTemplate(): bool
{
return $this->is_template;
}
function randomize(): void {
return;
}
}
class PicturedChallenge extends Challenge
{
protected string $image_url; // the URL of the corresponding image
function __construct(string $type, array $a = null)
{
parent::__construct($type);
$this->image_url = $a["image_url"] ?? "";
}
function setImageUrl(string $image_url): void
{
$this->image_url = $image_url;
}
function getImageUrl(): string
{
return $this->image_url;
}
function toArray(): array
{
$a = parent::toArray();
$a["image_url"] = $this->image_url;
return $a;
}
}
class SingleChoiceChallenge extends PicturedChallenge
{
private string $question; // the task title
private array $answers; // possible answers
private int $correct_answer; // the single correct answer
private int $player_answer; // answer given by the player
// -----------------
function __construct(array $a = null)
{
parent::__construct("singlechoice", $a);
$this->question = $a["question"] ?? "";
$this->answers = $a["answers"] ?? [];
$this->correct_answer = (int)($a["correct_answer"] ?? -1);
$this->player_answer = (int)($a["player_answer"] ?? -1);
}
function setQuestion(string $question): void
{
$this->question = $question;
}
function getQuestion(): string
{
return $this->question;
}
function addAnswer(string $answer): void
{
$this->answers[] = $answer;
}
function getAnswers(): array
{
return $this->answers;
}
function setCorrectAnswer(string $correct_answer): void
{
$this->correct_answer = $correct_answer;
}
function getCorrectAnswer(): string
{
return $this->correct_answer;
}
private function isAnswerIdInsideBounds($ansid): bool
{
return ($ansid >= 0) && ($ansid <= count($this->answers));
}
function saveAnswer(int|string $ans): bool
{
$ansidx = (int)($ans); // cast answer to integer as it is a number
if ($this->isAnswerIdInsideBounds($ansidx)) {
$this->player_answer = $ansidx;
return true;
}
return false;
}
function clearAnswer(int|string $ans): bool
{
$ansidx = (int)($ans); // cast answer to integer as it is a number
if ($this->isAnswerIdInsideBounds($ansidx)) {
$this->player_answer = -1;
return true;
}
return false;
}
public function getMark(): float
{
return ($this->player_answer == $this->correct_answer) ? 1.0 : 0.0;
}
function toArray(): array
{
$a = parent::toArray();
$a["question"] = $this->question;
$a["answers"] = $this->answers;
$a["correct_answer"] = $this->correct_answer;
if (!$this->isTemplate()) {
$a["player_answer"] = $this->player_answer;
}
return $a;
}
function randomize(): void{
//shuffle($this->answers); // shuffle answers
//$this->correct_answer = array_search($this->correct_answer, $this->answers); // remap correct answer
}
}
class ChallengeFactory
{
static function fromArray(array $a): Challenge|null
{
$type = $a["type"] ?? "singlechoice"; // if the type is missing, then it's a single choice challenge
switch ($type) {
case "singlechoice":
return new SingleChoiceChallenge($a);
}
return null;
}
static function constructFromCollection(array $c): array {
$chgs = [];
foreach ($c as $ch) {
$chgs[] = ChallengeFactory::fromArray($ch);
}
return $chgs;
} }
} }
@ -99,14 +320,23 @@ class Test extends AutoStoring
private function preprocessChallenges(): void private function preprocessChallenges(): void
{ {
foreach ($this->challenges as &$ch) { foreach ($this->challenges as &$ch) {
shuffle($ch["answers"]); // shuffle answers $ch->randomize();
$ch["correct_answer"] = array_search($ch["correct_answer"], $ch["answers"]); // remap correct answer
$ch["player_answer"] = -1; // create player answer field
} }
} }
// ------------- // -------------
function getMaxSumMark(): float
{
$msm = 0.0;
foreach ($this->challenges as &$ch) {
$msm += $ch->getMaxMark();
}
return $msm;
}
// -------------
// Store modifications. // Store modifications.
public function storeMods(): void public function storeMods(): void
{ {
@ -136,11 +366,11 @@ class Test extends AutoStoring
$this->endTime = $a["end_time"] ?? 0; $this->endTime = $a["end_time"] ?? 0;
$this->endLimitTime = $a["end_limit_time"] ?? 0; $this->endLimitTime = $a["end_limit_time"] ?? 0;
$this->repeatable = $a["repeatable"]; $this->repeatable = $a["repeatable"];
$this->challenges = $a["challenges"]; $this->challenges = ChallengeFactory::constructFromCollection($a["challenges"]);
if (isset($a["summary"])) { if (isset($a["summary"])) {
$this->summary = TestSummary::fromArray($a["summary"]); $this->summary = TestSummary::fromArray($a["summary"]);
} else { // backward compatibility } else { // backward compatibility
$this->summary = new TestSummary(count($a["challenges"]), 0); $this->summary = new TestSummary($this->getMaxSumMark(), 0);
} }
} else { // populating fields from Game and User objects } else { // populating fields from Game and User objects
$game = &$game_array; $game = &$game_array;
@ -150,7 +380,7 @@ class Test extends AutoStoring
// Fill-in basic properties // Fill-in basic properties
$this->gameId = $game->getId(); $this->gameId = $game->getId();
$this->gameName = $game->getName(); $this->gameName = $game->getName();
$this->challenges = $game->getChallenges(); $this->challenges = ChallengeFactory::constructFromCollection($game->getChallenges());
$this->preprocessChallenges(); $this->preprocessChallenges();
$this->nickname = $user->getNickname(); $this->nickname = $user->getNickname();
@ -169,7 +399,7 @@ class Test extends AutoStoring
$this->repeatable = $gp["repeatable"]; $this->repeatable = $gp["repeatable"];
// Create a blank summary // Create a blank summary
$this->summary = new TestSummary(count($this->challenges), 0); $this->summary = new TestSummary($this->getMaxSumMark(), 0);
} }
// auto-conclude time-constrained test if expired // auto-conclude time-constrained test if expired
@ -182,6 +412,11 @@ class Test extends AutoStoring
// Convert test to array. // Convert test to array.
function toArray(array $omit = []): array function toArray(array $omit = []): array
{ {
$chgs = [];
foreach ($this->challenges as $ch) {
$chgs[] = $ch->toArray();
}
$a = [ $a = [
"_id" => $this->id, "_id" => $this->id,
"gameid" => $this->gameId, "gameid" => $this->gameId,
@ -193,7 +428,7 @@ class Test extends AutoStoring
"end_time" => $this->endTime, "end_time" => $this->endTime,
"end_limit_time" => $this->endLimitTime, "end_limit_time" => $this->endLimitTime,
"repeatable" => $this->repeatable, "repeatable" => $this->repeatable,
"challenges" => $this->challenges, "challenges" => $chgs,
"summary" => $this->summary->toArray() "summary" => $this->summary->toArray()
]; ];
@ -211,12 +446,16 @@ class Test extends AutoStoring
return count($this->challenges); return count($this->challenges);
} }
function isChallengeIdInsideBounds(int $chidx): bool {
return ($chidx >= 0) && ($chidx < $this->getChallengeCount());
}
// Save answer. Asserting $safe prevents saving answers to a concluded test. // Save answer. Asserting $safe prevents saving answers to a concluded test.
function saveAnswer(int $chidx, int $ansidx, bool $safe = true): bool function saveAnswer(int $chidx, string $ans, bool $safe = true): bool
{ {
if (!$safe || $this->state === self::TEST_ONGOING) { if (!$safe || $this->state === self::TEST_ONGOING) {
if (($chidx < $this->getChallengeCount()) && ($ansidx < $this->challenges[$chidx]["answers"])) { if ($this->isChallengeIdInsideBounds($chidx)) {
$this->challenges[$chidx]["player_answer"] = $ansidx; $this->challenges[$chidx]->saveAnswer($ans);
$this->commitMods(); $this->commitMods();
return true; return true;
} }
@ -228,8 +467,8 @@ class Test extends AutoStoring
function clearAnswer(int $chidx, bool $safe = true): bool function clearAnswer(int $chidx, bool $safe = true): bool
{ {
if (!$safe || $this->state === self::TEST_ONGOING) { if (!$safe || $this->state === self::TEST_ONGOING) {
if ($chidx < $this->getChallengeCount()) { if ($this->isChallengeIdInsideBounds($chidx)) {
$this->challenges[$chidx]["player_answer"] = -1; $this->challenges[$chidx]->clearAnswer();
$this->commitMods(); $this->commitMods();
return true; return true;
} }
@ -240,18 +479,16 @@ class Test extends AutoStoring
// Conclude test. // Conclude test.
function concludeTest(): void function concludeTest(): void
{ {
// check the answers // summarize points
$cans_n = 0; // number of correct answers $mark_sum = 0.0;
foreach ($this->challenges as &$ch) { foreach ($this->challenges as &$ch) {
if ($ch["player_answer"] === $ch["correct_answer"]) { $mark_sum += $ch->getMark();
$cans_n++;
}
} }
// set state and fill summary // set state and fill summary
$this->state = TEST_CONCLUDED; $this->state = TEST_CONCLUDED;
$this->endTime = time(); $this->endTime = time();
$this->summary->setCorrectAnswerN($cans_n); $this->summary->setMark($mark_sum);
// save test // save test
$this->commitMods(); $this->commitMods();

View File

@ -4,7 +4,7 @@ require_once "class/TestMgr.php";
const longopts = [ const longopts = [
"action:", // execute some CLI action "action:", // execute some CLI action
"tick" // tick timed objects (e.g. timed tests) "tick", // tick timed objects (e.g. timed tests)
]; ];
$options = getopt("", longopts); $options = getopt("", longopts);

View File

@ -1,5 +1,7 @@
<?php <?php
ini_set("display_errors", true);
require_once "check_maintenance.php"; require_once "check_maintenance.php";
//ini_set('display_startup_errors', '1'); //ini_set('display_startup_errors', '1');

View File

@ -46,7 +46,7 @@ function populate_infobox(test_data, view_only) {
time_left_s--; time_left_s--;
print_timer(); print_timer();
if (time_left_s <= 0) { if (time_left_s <= 0) {
populate_all(test_data["_id"], test_data["gameid"]); populate_all(test_data["_id"], test_data["gameid"], false);
clearInterval(INTERVAL_HANDLE); clearInterval(INTERVAL_HANDLE);
INTERVAL_HANDLE = null; INTERVAL_HANDLE = null;
} }
@ -183,6 +183,6 @@ function submit_test() {
testid: TEST_DATA["_id"] testid: TEST_DATA["_id"]
} }
request(req).then(resp => { request(req).then(resp => {
populate_all(TEST_DATA["_id"], TEST_DATA["gameid"]); populate_all(TEST_DATA["_id"], TEST_DATA["gameid"], false);
}); });
} }

View File

@ -5,7 +5,9 @@
<title>SpreadQuiz :: Karbantartás</title> <title>SpreadQuiz :: Karbantartás</title>
</head> </head>
<body> <body>
<img src="media/maintenance.png" width="180"> <section style="margin: 0 auto; padding-top: 10ex; text-align: center">
<h3> Az oldal karbantartás alatt áll!</h3> <img src="media/maintenance.png" style="width: 16vw">
<h3 style="font-family: 'Monaco', monospace"> Az oldal karbantartás alatt áll!</h3>
</section>
</body> </body>
</html> </html>