- 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;
if ($version < 2) { // update to game version 2
@ -49,6 +49,13 @@ class Game extends AutoStoring
$a["version"] = 2;
}
if ($version < 3) {
return;
//$a["version"] = 3;
}
}
// Store modifications.
@ -106,7 +113,7 @@ class Game extends AutoStoring
static function fromArray(GameMgr &$gameMgr, array $a): Game
{
$id = $a["_id"] ?? -1;
self::patchUpGameDate($a);
self::patchUpGameData($a);
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"]);
}
@ -136,7 +143,7 @@ class Game extends AutoStoring
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
{
// load challenges
@ -185,7 +192,7 @@ class Game extends AutoStoring
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
{
// convert text encoding into UTF-8

View File

@ -79,7 +79,7 @@ class Answer
}
}
class Challenge
class ChallengeReport
{
private string $question;
private array $answers;
@ -152,7 +152,7 @@ class ReportSection
function __construct(string $title, array $challenges)
{
$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

View File

@ -13,15 +13,15 @@ const TEST_CONCLUDED = "concluded";
class TestSummary
{
public int $challengeN; // Number of challenges
public int $correctAnswerN; // Number of correct answers
public int $maxMark; // Number of challenges
public int $mark; // Number of correct answers
private float $percentage; // Ratio of correct answers
// Calculate percentage.
private function calculatePercentage(): void
{
if ($this->challengeN > 0) {
$this->percentage = $this->correctAnswerN / (double)$this->challengeN * 100.0;
if ($this->maxMark > 0) {
$this->percentage = $this->mark / (double)$this->maxMark * 100.0;
} else { // avoid division by zero
$this->percentage = 0.0;
}
@ -29,45 +29,266 @@ class TestSummary
function __construct(int $challengeN, int $correctAnswerN)
{
$this->challengeN = $challengeN;
$this->correctAnswerN = $correctAnswerN;
$this->maxMark = $challengeN;
$this->mark = $correctAnswerN;
$this->calculatePercentage();
}
// Get challenge count.
function getChallengeN(): int
function getMaxMark(): int
{
return $this->challengeN;
return $this->maxMark;
}
// 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();
}
// Get ratio of correct results.
function getPercentage(): float
{
return ($this->correctAnswerN * 100.0) / $this->challengeN;
return ($this->mark * 100.0) / $this->maxMark;
}
// Build from array.
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.
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
{
foreach ($this->challenges as &$ch) {
shuffle($ch["answers"]); // shuffle answers
$ch["correct_answer"] = array_search($ch["correct_answer"], $ch["answers"]); // remap correct answer
$ch["player_answer"] = -1; // create player answer field
$ch->randomize();
}
}
// -------------
function getMaxSumMark(): float
{
$msm = 0.0;
foreach ($this->challenges as &$ch) {
$msm += $ch->getMaxMark();
}
return $msm;
}
// -------------
// Store modifications.
public function storeMods(): void
{
@ -136,11 +366,11 @@ class Test extends AutoStoring
$this->endTime = $a["end_time"] ?? 0;
$this->endLimitTime = $a["end_limit_time"] ?? 0;
$this->repeatable = $a["repeatable"];
$this->challenges = $a["challenges"];
$this->challenges = ChallengeFactory::constructFromCollection($a["challenges"]);
if (isset($a["summary"])) {
$this->summary = TestSummary::fromArray($a["summary"]);
} 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
$game = &$game_array;
@ -150,7 +380,7 @@ class Test extends AutoStoring
// Fill-in basic properties
$this->gameId = $game->getId();
$this->gameName = $game->getName();
$this->challenges = $game->getChallenges();
$this->challenges = ChallengeFactory::constructFromCollection($game->getChallenges());
$this->preprocessChallenges();
$this->nickname = $user->getNickname();
@ -169,7 +399,7 @@ class Test extends AutoStoring
$this->repeatable = $gp["repeatable"];
// 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
@ -182,6 +412,11 @@ class Test extends AutoStoring
// Convert test to array.
function toArray(array $omit = []): array
{
$chgs = [];
foreach ($this->challenges as $ch) {
$chgs[] = $ch->toArray();
}
$a = [
"_id" => $this->id,
"gameid" => $this->gameId,
@ -193,7 +428,7 @@ class Test extends AutoStoring
"end_time" => $this->endTime,
"end_limit_time" => $this->endLimitTime,
"repeatable" => $this->repeatable,
"challenges" => $this->challenges,
"challenges" => $chgs,
"summary" => $this->summary->toArray()
];
@ -211,12 +446,16 @@ class Test extends AutoStoring
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.
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 (($chidx < $this->getChallengeCount()) && ($ansidx < $this->challenges[$chidx]["answers"])) {
$this->challenges[$chidx]["player_answer"] = $ansidx;
if ($this->isChallengeIdInsideBounds($chidx)) {
$this->challenges[$chidx]->saveAnswer($ans);
$this->commitMods();
return true;
}
@ -228,8 +467,8 @@ class Test extends AutoStoring
function clearAnswer(int $chidx, bool $safe = true): bool
{
if (!$safe || $this->state === self::TEST_ONGOING) {
if ($chidx < $this->getChallengeCount()) {
$this->challenges[$chidx]["player_answer"] = -1;
if ($this->isChallengeIdInsideBounds($chidx)) {
$this->challenges[$chidx]->clearAnswer();
$this->commitMods();
return true;
}
@ -240,18 +479,16 @@ class Test extends AutoStoring
// Conclude test.
function concludeTest(): void
{
// check the answers
$cans_n = 0; // number of correct answers
// summarize points
$mark_sum = 0.0;
foreach ($this->challenges as &$ch) {
if ($ch["player_answer"] === $ch["correct_answer"]) {
$cans_n++;
}
$mark_sum += $ch->getMark();
}
// set state and fill summary
$this->state = TEST_CONCLUDED;
$this->endTime = time();
$this->summary->setCorrectAnswerN($cans_n);
$this->summary->setMark($mark_sum);
// save test
$this->commitMods();

View File

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

View File

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

View File

@ -46,7 +46,7 @@ function populate_infobox(test_data, view_only) {
time_left_s--;
print_timer();
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);
INTERVAL_HANDLE = null;
}
@ -183,6 +183,6 @@ function submit_test() {
testid: TEST_DATA["_id"]
}
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>
</head>
<body>
<img src="media/maintenance.png" width="180">
<h3> Az oldal karbantartás alatt áll!</h3>
<section style="margin: 0 auto; padding-top: 10ex; text-align: center">
<img src="media/maintenance.png" style="width: 16vw">
<h3 style="font-family: 'Monaco', monospace"> Az oldal karbantartás alatt áll!</h3>
</section>
</body>
</html>