Compare commits
	
		
			No commits in common. "challenge_modularization" and "master" have entirely different histories.
		
	
	
		
			challenge_
			...
			master
		
	
		
							
								
								
									
										572
									
								
								class/Game.php
									
									
									
									
									
								
							
							
						
						
									
										572
									
								
								class/Game.php
									
									
									
									
									
								
							@ -1,572 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "vendor/autoload.php";
 | 
			
		||||
 | 
			
		||||
require_once "AutoStoring.php";
 | 
			
		||||
 | 
			
		||||
require_once "Utils.php";
 | 
			
		||||
 | 
			
		||||
class Game extends AutoStoring
 | 
			
		||||
{
 | 
			
		||||
    public const DEFAULT_GAME_PROPERTIES = [
 | 
			
		||||
        "forward_only" => false, // player may traverse back and forth between tasks
 | 
			
		||||
        "time_limit" => 0, // no time limit; otherwise, this field indicates time limit in seconds
 | 
			
		||||
        "repeatable" => false // this test can be taken multiple times
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    public const CURRENT_GAME_VERSION = 2; // MUST BE INCREMENTED!!
 | 
			
		||||
 | 
			
		||||
    // --------
 | 
			
		||||
    private int $id; // Game's ID
 | 
			
		||||
    private string $name; // Game's name
 | 
			
		||||
    private string $owner; // Game's owner
 | 
			
		||||
    private array $contributors; // Contributors to the game
 | 
			
		||||
    private string $description; // Game's description
 | 
			
		||||
    private bool $gameFileIsPresent; // Indicates if game CSV is in place
 | 
			
		||||
    private array $properties; // Collection of several game properties
 | 
			
		||||
    private bool $public; // Is this game publicly available?
 | 
			
		||||
    private string $publicId; // Public-accessible ID
 | 
			
		||||
    private int $VERSION; // Game representation version (used during updates)
 | 
			
		||||
    private GameMgr $gameMgr; // Game manager managing this instance
 | 
			
		||||
    private bool $tasksLoaded; // Indicates if tasks have been fetched
 | 
			
		||||
    private array $tasks; // Tasks
 | 
			
		||||
 | 
			
		||||
    // -------
 | 
			
		||||
 | 
			
		||||
    static public function genPublicId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return uniqid("p");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------
 | 
			
		||||
 | 
			
		||||
    static private function patchUpGameData(array &$a): void
 | 
			
		||||
    {
 | 
			
		||||
        $version = $a["version"] ?? 0;
 | 
			
		||||
        if ($version < 2) { // update to game version 2
 | 
			
		||||
            if (!key_exists("public_id", $a)) {
 | 
			
		||||
                $a["public"] = false;
 | 
			
		||||
                $a["public_id"] = self::genPublicId();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $a["version"] = 2;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Store modifications.
 | 
			
		||||
    public function storeMods(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->gameMgr->updateGame($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Commit modifications.
 | 
			
		||||
    function commitMods(): void
 | 
			
		||||
    {
 | 
			
		||||
        //$this->patchUpGameDate();
 | 
			
		||||
        parent::commitMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Load game tasks.
 | 
			
		||||
    public function loadTasks(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->isGameFileIsPresent() && !$this->tasksLoaded) { // load if file is present
 | 
			
		||||
            $this->tasks = TaskFactory::constructFromCollection(json_decode(file_get_contents($this->getGameFile()), true));
 | 
			
		||||
            foreach ($this->tasks as &$ch) {
 | 
			
		||||
                $ch->setTemplate(true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Save tasks.
 | 
			
		||||
    public function saveTasks(): void
 | 
			
		||||
    {
 | 
			
		||||
        file_put_contents($this->getGameFile(), json_encode($this->tasks)); // store tasks in JSON-format
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------
 | 
			
		||||
 | 
			
		||||
    function __construct(GameMgr &$gameMgr, string $name, string $description = "", int $id = -1, string $owner = "",
 | 
			
		||||
                         array   $contributors = [], bool $gameFileIsPresent = false, array $properties = [],
 | 
			
		||||
                         bool    $public = false, string $publicId = "", int $version = 2)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->tasksLoaded = false;
 | 
			
		||||
 | 
			
		||||
        $this->gameMgr = $gameMgr;
 | 
			
		||||
        $this->id = $id;
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->description = $description;
 | 
			
		||||
        $this->owner = $owner;
 | 
			
		||||
        $this->contributors = $contributors;
 | 
			
		||||
        $this->gameFileIsPresent = $gameFileIsPresent;
 | 
			
		||||
        $this->properties = $properties;
 | 
			
		||||
        $this->public = $public;
 | 
			
		||||
        $this->publicId = $publicId;
 | 
			
		||||
        $this->VERSION = $version;
 | 
			
		||||
        $this->tasks = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create game from array representation.
 | 
			
		||||
    static function fromArray(GameMgr &$gameMgr, array $a): Game
 | 
			
		||||
    {
 | 
			
		||||
        $id = $a["_id"] ?? -1;
 | 
			
		||||
        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"]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const OMIT_ADVANCED_FIELDS = ["contributors", "game_file_is_present", "properties", "public", "public_id", "version"];
 | 
			
		||||
 | 
			
		||||
    // Convert game to array representation.
 | 
			
		||||
    function toArray(array $omit = []): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = [
 | 
			
		||||
            "_id" => $this->id,
 | 
			
		||||
            "name" => $this->name,
 | 
			
		||||
            "description" => $this->description,
 | 
			
		||||
            "owner" => $this->owner,
 | 
			
		||||
            "contributors" => $this->contributors,
 | 
			
		||||
            "game_file_present" => $this->gameFileIsPresent,
 | 
			
		||||
            "properties" => $this->properties,
 | 
			
		||||
            "public" => $this->public,
 | 
			
		||||
            "public_id" => $this->publicId,
 | 
			
		||||
            "version" => $this->VERSION,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        foreach ($omit as $field) {
 | 
			
		||||
            unset($a[$field]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Export tasks to a CSV file. TODO: ez csak a feleletválasztóshoz lesz jó
 | 
			
		||||
    function exportTasksToCSV(&$f): void
 | 
			
		||||
    {
 | 
			
		||||
        // load tasks
 | 
			
		||||
        $this->loadTasks();
 | 
			
		||||
 | 
			
		||||
        // populate CSV file
 | 
			
		||||
        foreach ($this->tasks as $ch) {
 | 
			
		||||
            if ($ch->getType() === "singlechoice") {
 | 
			
		||||
                $csvline = [
 | 
			
		||||
                    $ch["question"],
 | 
			
		||||
                    $ch["image_url"],
 | 
			
		||||
                ];
 | 
			
		||||
                $csvline = array_merge($csvline, $ch["answers"]);
 | 
			
		||||
                fputcsv($f, $csvline);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get game directory NAME with path. Does not check if the game directory exists or not.
 | 
			
		||||
    function getGameDir(): string
 | 
			
		||||
    {
 | 
			
		||||
        return self::getGameDirById($this->getId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getAccompanyingFilePath(string $file_name): string {
 | 
			
		||||
        return $this->getGameDir() . DIRECTORY_SEPARATOR . $file_name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static function getGameDirById(int $id): string {
 | 
			
		||||
        return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get game file NAME with path. Does not check whether the game file is in place or not.
 | 
			
		||||
    function getGameFile(): string
 | 
			
		||||
    {
 | 
			
		||||
        return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId() . DIRECTORY_SEPARATOR . GAME_FILE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Is the given user the owner of the game?
 | 
			
		||||
    function isUserOwner(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->owner === $nickname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Is the given user a contributor of the game?
 | 
			
		||||
    function isUserContributor(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($nickname, $this->contributors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Is user contributor or owner?
 | 
			
		||||
    function isUserContributorOrOwner(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->isUserContributor($nickname) || $this->isUserOwner($nickname);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const CSV_ENCODINGS = ["UTF-8", "Windows-1252"];
 | 
			
		||||
 | 
			
		||||
    // Import tasks from a CSV table. TODO: ez csak a feleletválasztós betöltésére lesz jó
 | 
			
		||||
    function importTasksFromCSV(string $csv_path): array
 | 
			
		||||
    {
 | 
			
		||||
        // convert text encoding into UTF-8
 | 
			
		||||
        $data = file_get_contents($csv_path);
 | 
			
		||||
        $encoding = "UNKNOWN";
 | 
			
		||||
        foreach (self::CSV_ENCODINGS as $enc) { // detect encoding
 | 
			
		||||
            if (mb_check_encoding($data, $enc)) {
 | 
			
		||||
                $encoding = $enc;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($encoding !== "UNKNOWN") { // if encoding has been detected successfully
 | 
			
		||||
            $data = mb_convert_encoding($data, "UTF-8", $encoding);
 | 
			
		||||
            file_put_contents($csv_path, $data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // clear tasks
 | 
			
		||||
        $this->tasks = [];
 | 
			
		||||
 | 
			
		||||
        // load filled CSV file
 | 
			
		||||
        $f = fopen($csv_path, "r");
 | 
			
		||||
        if (!$f) { // failed to open file
 | 
			
		||||
            return ["n" => 0, "encoding" => $encoding];
 | 
			
		||||
        }
 | 
			
		||||
        while ($csvline = fgetcsv($f)) {
 | 
			
		||||
            // skip empty lines
 | 
			
		||||
            if (trim(implode("", $csvline)) === "") {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            if (count($csvline) >= 3) {
 | 
			
		||||
                // construct task record
 | 
			
		||||
                $a = [
 | 
			
		||||
                    "question" => trim($csvline[0]),
 | 
			
		||||
                    "image_data" => trim($csvline[1]),
 | 
			
		||||
                    "correct_answer" => 0,
 | 
			
		||||
                    "answers" => array_filter(array_slice($csvline, 2), function ($v) {
 | 
			
		||||
                        return trim($v) !== "";
 | 
			
		||||
                    })
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
                // if an image is attached to the task, then give a random name to the image
 | 
			
		||||
                if ($a["image_data"] !== "") {
 | 
			
		||||
                    $a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]);
 | 
			
		||||
                    $a["image_type"] = "url";
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // store the task
 | 
			
		||||
                $this->tasks[] = new SingleChoiceTask($a);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        fclose($f);
 | 
			
		||||
 | 
			
		||||
        // save tasks
 | 
			
		||||
        $this->saveTasks();
 | 
			
		||||
 | 
			
		||||
        // update game with game file present
 | 
			
		||||
        $this->gameFileIsPresent = true;
 | 
			
		||||
 | 
			
		||||
        // store modifications
 | 
			
		||||
        $this->commitMods();
 | 
			
		||||
 | 
			
		||||
        return ["n" => count($this->tasks), "encoding" => $encoding];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static function getTableVersion(string &$cA1): string
 | 
			
		||||
    {
 | 
			
		||||
        if (str_starts_with($cA1, "#:V")) {
 | 
			
		||||
            return trim(substr($cA1, 3));
 | 
			
		||||
        } else {
 | 
			
		||||
            return "1";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function obfuscateAttachedImage(string $old_img_name): string
 | 
			
		||||
    {
 | 
			
		||||
        $ext = pathinfo($old_img_name, PATHINFO_EXTENSION);
 | 
			
		||||
        $ext = ($ext !== "") ? ("." . $ext) : $ext;
 | 
			
		||||
        $new_img_name = uniqid("img_", true) . $ext;
 | 
			
		||||
 | 
			
		||||
        // rename the actual file
 | 
			
		||||
        $old_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $old_img_name;
 | 
			
		||||
        $new_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $new_img_name;
 | 
			
		||||
        rename($old_img_path, $new_img_path);
 | 
			
		||||
 | 
			
		||||
        return $new_img_name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function importTasksFromTableV1(array &$table): array
 | 
			
		||||
    {
 | 
			
		||||
        $n = count($table);
 | 
			
		||||
        for ($i = 1; $i < $n; $i++) {
 | 
			
		||||
            $row = &$table[$i]; // fetch row
 | 
			
		||||
            $a = [ // create initializing array
 | 
			
		||||
                "question" => trim($row[0]),
 | 
			
		||||
                "image_data" => trim($row[1]),
 | 
			
		||||
                "correct_answer" => 0,
 | 
			
		||||
                "answers" => array_filter(array_slice($row, 2), function ($v) {
 | 
			
		||||
                    return trim($v ?? "") !== "";
 | 
			
		||||
                })
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            // obfuscate image filename
 | 
			
		||||
            if ($a["image_data"] !== "") {
 | 
			
		||||
                $a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]);
 | 
			
		||||
                $a["image_type"] = "url";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // create the task
 | 
			
		||||
            $this->tasks[] = new SingleChoiceTask($a);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return ["n" => $n, "encoding" => "automatikusan konvertált"];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static function getTableColumnIndices(array &$header): array
 | 
			
		||||
    {
 | 
			
		||||
        $columns = [];
 | 
			
		||||
        for ($i = 1; $i < count($header); $i++) { // skip the first column as it is metadata
 | 
			
		||||
            $label = $header[$i];
 | 
			
		||||
            if (($label ?? "") !== "") {
 | 
			
		||||
                $columns[$label] = $i;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return $columns;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static function getFirstUnlabeledColumn(array &$header): int
 | 
			
		||||
    {
 | 
			
		||||
        for ($i = 0; $i < count($header); $i++) {
 | 
			
		||||
            if (trim($header[$i] ?? "") === "") {
 | 
			
		||||
                return $i;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static function explodeFlags(string $fs): array
 | 
			
		||||
    {
 | 
			
		||||
        $flags = explode(",", trim($fs));
 | 
			
		||||
        return array_filter($flags, fn($v) => trim($v) !== "");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function importTasksFromTableV2(array &$table): array
 | 
			
		||||
    {
 | 
			
		||||
        $result = ["n" => 0, "encoding" => "automatikusan konvertált"]; // prepare result
 | 
			
		||||
        $n = count($table); // get number of entries (including header)
 | 
			
		||||
        if ($n === 0) { // cannot import an empty table
 | 
			
		||||
            return $result;
 | 
			
		||||
        }
 | 
			
		||||
        $header = &$table[0]; // extract header
 | 
			
		||||
 | 
			
		||||
        $fuc = Game::getFirstUnlabeledColumn($header); // get first unlabeled column
 | 
			
		||||
        if ($fuc === -1) { // if there's no data, then it is impossible to create the tasks
 | 
			
		||||
            return $result;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $columns = Game::getTableColumnIndices($header); // fetch column names
 | 
			
		||||
 | 
			
		||||
        // start iterating over tasks
 | 
			
		||||
        $count = 0;
 | 
			
		||||
        for ($i = 1; $i < $n; $i++) {
 | 
			
		||||
            $row = &$table[$i]; // fetch row
 | 
			
		||||
 | 
			
		||||
            $flags = Game::explodeFlags($row[0] ?? ""); // get flags
 | 
			
		||||
            if (in_array("hidden", $flags)) { // skip hidden tasks
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // count in this task
 | 
			
		||||
            $count++;
 | 
			
		||||
 | 
			
		||||
            // prepare a function that looks up the fields referenced by their labels
 | 
			
		||||
            $select_fn = function (array $cols) use (&$row, &$columns) {
 | 
			
		||||
                for ($i = 0; $i < count($cols); $i++) {
 | 
			
		||||
                    if (isset($columns[$cols[$i]])) {
 | 
			
		||||
                        return trim($row[$columns[$cols[$i]]]);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return "";
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            // prepare a function that extracts all unlabeled fields
 | 
			
		||||
            $extract_unlabeled_fn = fn() => array_filter(array_slice($row, $fuc), function ($v) {
 | 
			
		||||
                return trim($v ?? "") !== "";
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // fetch generic fields
 | 
			
		||||
            $a = [
 | 
			
		||||
                "flags" => $flags,
 | 
			
		||||
                "type" => strtolower($select_fn(["Típus", "Type"])),
 | 
			
		||||
                "image_data" => $select_fn(["Kép", "Image"]),
 | 
			
		||||
                "question" => $select_fn(["Kérdés", "Question"]),
 | 
			
		||||
                "lua_script" => $select_fn(["Lua"]),
 | 
			
		||||
                "lua_params" => Utils::str2kv($select_fn(["LuaParam"])),
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            // convert into
 | 
			
		||||
            switch ($a["type"]) {
 | 
			
		||||
                case "singlechoice":
 | 
			
		||||
                    $a["answers"] = $extract_unlabeled_fn();
 | 
			
		||||
                    $a["correct_answer"] = 0;
 | 
			
		||||
                    break;
 | 
			
		||||
                case "openended":
 | 
			
		||||
                    $a["correct_answer"] = $extract_unlabeled_fn();
 | 
			
		||||
                    break;
 | 
			
		||||
                case "numberconversion":
 | 
			
		||||
                    $a["instruction"] = $row[$fuc];
 | 
			
		||||
                    break;
 | 
			
		||||
                case "truthtable":
 | 
			
		||||
                case "logicfunction":
 | 
			
		||||
                    $a["input_variables"] = Utils::str2a($row[$fuc]);
 | 
			
		||||
                    $a["output_variable"] = $row[$fuc + 1];
 | 
			
		||||
                    $a["expression"] = $row[$fuc + 2];
 | 
			
		||||
                    break;
 | 
			
		||||
                case "verilog":
 | 
			
		||||
                    $a["test_bench_fn"] = $row[$fuc];
 | 
			
		||||
                    $a["correct_answer"] = file_get_contents($this->getAccompanyingFilePath($row[$fuc + 1]));
 | 
			
		||||
                    if (isset($row[$fuc + 2])) {
 | 
			
		||||
                        $a["player_answer"] = file_get_contents($this->getAccompanyingFilePath($row[$fuc + 2]));
 | 
			
		||||
                    } else {
 | 
			
		||||
                        $a["player_answer"] = "";
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // obfuscate image filename
 | 
			
		||||
            if ($a["image_data"] !== "") {
 | 
			
		||||
                if (in_array("codeimage", $flags)) {
 | 
			
		||||
                    $a["image_type"] = "code";
 | 
			
		||||
                } else {
 | 
			
		||||
                    $a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]);
 | 
			
		||||
                    $a["image_type"] = "url";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // generate the task
 | 
			
		||||
            $this->tasks[] = TaskFactory::fromArray($a, $this);
 | 
			
		||||
 | 
			
		||||
            // assign scoring strategy
 | 
			
		||||
            $sct = $select_fn(["Pontozás", "Scoring"]);
 | 
			
		||||
            if ($sct !== "") {
 | 
			
		||||
                $sct_fields = Utils::str2kv($sct);
 | 
			
		||||
                foreach ($sct_fields as $key => $value) {
 | 
			
		||||
                    $sct_fields[$key] = $value;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $result["n"] = $count;
 | 
			
		||||
        return $result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function importTasksFromTable(array &$table): array
 | 
			
		||||
    {
 | 
			
		||||
        // clear tasks
 | 
			
		||||
        $this->tasks = [];
 | 
			
		||||
 | 
			
		||||
        // get table version
 | 
			
		||||
        $vs = Game::getTableVersion($table[0][0]);
 | 
			
		||||
 | 
			
		||||
        // continue processing based on table version
 | 
			
		||||
        $result = ["n" => 0, "encoding" => "ismeretlen"];
 | 
			
		||||
        switch ($vs) {
 | 
			
		||||
            case "1":
 | 
			
		||||
                $result = $this->importTasksFromTableV1($table);
 | 
			
		||||
                break;
 | 
			
		||||
            case "2":
 | 
			
		||||
                $result = $this->importTasksFromTableV2($table);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // if the number of imported tasks is not zero, then it was a successful import
 | 
			
		||||
        $this->gameFileIsPresent = false; // assume no game file present
 | 
			
		||||
        if ($result["n"] > 0) {
 | 
			
		||||
            $this->saveTasks();             // save tasks
 | 
			
		||||
            $this->gameFileIsPresent = true;             // update game with game file present
 | 
			
		||||
            $this->commitMods(); // store modifications
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ---------
 | 
			
		||||
 | 
			
		||||
    public function getName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setName(string $name): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getOwner(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->owner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setOwner(string $owner): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->owner = $owner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getContributors(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->contributors;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setContributors(array $contributors): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->contributors = $contributors;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setDescription(string $description): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->description = $description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isGameFileIsPresent(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->gameFileIsPresent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setProperties(array $properties): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->properties = $properties;
 | 
			
		||||
        $this->commitMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function& getProperties(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->properties;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isPublic(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->public;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getPublicId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->publicId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setPublic(bool $public): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->public = $public;
 | 
			
		||||
        $this->commitMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTasks(): array
 | 
			
		||||
    {
 | 
			
		||||
        $this->loadTasks();
 | 
			
		||||
        return $this->tasks;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -4,7 +4,344 @@ require_once "vendor/autoload.php";
 | 
			
		||||
 | 
			
		||||
require_once "AutoStoring.php";
 | 
			
		||||
 | 
			
		||||
require_once "Game.php";
 | 
			
		||||
class Game extends AutoStoring
 | 
			
		||||
{
 | 
			
		||||
    public const DEFAULT_GAME_PROPERTIES = [
 | 
			
		||||
        "forward_only" => false, // player may traverse back and forth between challenges
 | 
			
		||||
        "time_limit" => 0, // no time limit; otherwise, this field indicates time limit in seconds
 | 
			
		||||
        "repeatable" => false // this test can be taken multiple times
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    public const CURRENT_GAME_VERSION = 2; // MUST BE INCREMENTED!!
 | 
			
		||||
 | 
			
		||||
    // --------
 | 
			
		||||
    private int $id; // Game's ID
 | 
			
		||||
    private string $name; // Game's name
 | 
			
		||||
    private string $owner; // Game's owner
 | 
			
		||||
    private array $contributors; // Contributors to the game
 | 
			
		||||
    private string $description; // Game's description
 | 
			
		||||
    private bool $gameFileIsPresent; // Indicates if game CSV is in place
 | 
			
		||||
    private array $properties; // Collection of several game properties
 | 
			
		||||
    private bool $public; // Is this game publicly available?
 | 
			
		||||
    private string $publicId; // Public-accessible ID
 | 
			
		||||
    private int $VERSION; // Game representation version (used during updates)
 | 
			
		||||
    private GameMgr $gameMgr; // Game manager managing this instance
 | 
			
		||||
    private bool $challengesLoaded; // Indicates if challenges have been fetched
 | 
			
		||||
    private array $challenges; // Challenges
 | 
			
		||||
 | 
			
		||||
    // -------
 | 
			
		||||
 | 
			
		||||
    static public function genPublicId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return uniqid("p");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------
 | 
			
		||||
 | 
			
		||||
    static private function patchUpGameDate(array &$a) : void
 | 
			
		||||
    {
 | 
			
		||||
        $version = $a["version"] ?? 0;
 | 
			
		||||
        if ($version < 2) { // update to game version 2
 | 
			
		||||
            if (!key_exists("public_id", $a)) {
 | 
			
		||||
                $a["public"] = false;
 | 
			
		||||
                $a["public_id"] = self::genPublicId();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $a["version"] = 2;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Store modifications.
 | 
			
		||||
    public function storeMods() : void
 | 
			
		||||
    {
 | 
			
		||||
        $this->gameMgr->updateGame($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Commit modifications.
 | 
			
		||||
    function commitMods(): void
 | 
			
		||||
    {
 | 
			
		||||
        //$this->patchUpGameDate();
 | 
			
		||||
        parent::commitMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Load game challenges.
 | 
			
		||||
    public function loadChallenges(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->isGameFileIsPresent() && !$this->challengesLoaded) { // load if file is present
 | 
			
		||||
            $this->challenges = json_decode(file_get_contents($this->getGameFile()), true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Save challenges.
 | 
			
		||||
    public function saveChallenges(): void
 | 
			
		||||
    {
 | 
			
		||||
        file_put_contents($this->getGameFile(), json_encode($this->challenges)); // store challenges in JSON-format
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------
 | 
			
		||||
 | 
			
		||||
    function __construct(GameMgr &$gameMgr, string $name, string $description = "", int $id = -1, string $owner = "",
 | 
			
		||||
                         array   $contributors = [], bool $gameFileIsPresent = false, array $properties = [],
 | 
			
		||||
                         bool    $public = false, string $publicId = "", int $version = 2)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->challengesLoaded = false;
 | 
			
		||||
 | 
			
		||||
        $this->gameMgr = $gameMgr;
 | 
			
		||||
        $this->id = $id;
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->description = $description;
 | 
			
		||||
        $this->owner = $owner;
 | 
			
		||||
        $this->contributors = $contributors;
 | 
			
		||||
        $this->gameFileIsPresent = $gameFileIsPresent;
 | 
			
		||||
        $this->properties = $properties;
 | 
			
		||||
        $this->public = $public;
 | 
			
		||||
        $this->publicId = $publicId;
 | 
			
		||||
        $this->VERSION = $version;
 | 
			
		||||
        $this->challenges = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create game from array representation.
 | 
			
		||||
    static function fromArray(GameMgr &$gameMgr, array $a): Game
 | 
			
		||||
    {
 | 
			
		||||
        $id = $a["_id"] ?? -1;
 | 
			
		||||
        self::patchUpGameDate($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"]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const OMIT_ADVANCED_FIELDS = ["contributors", "game_file_is_present", "properties", "public", "public_id", "version"];
 | 
			
		||||
 | 
			
		||||
    // Convert game to array representation.
 | 
			
		||||
    function toArray(array $omit = []): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = [
 | 
			
		||||
            "_id" => $this->id,
 | 
			
		||||
            "name" => $this->name,
 | 
			
		||||
            "description" => $this->description,
 | 
			
		||||
            "owner" => $this->owner,
 | 
			
		||||
            "contributors" => $this->contributors,
 | 
			
		||||
            "game_file_present" => $this->gameFileIsPresent,
 | 
			
		||||
            "properties" => $this->properties,
 | 
			
		||||
            "public" => $this->public,
 | 
			
		||||
            "public_id" => $this->publicId,
 | 
			
		||||
            "version" => $this->VERSION,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        foreach ($omit as $field) {
 | 
			
		||||
            unset($a[$field]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Export challenges to a CSV file.
 | 
			
		||||
    function exportChallengesToCSV(&$f): void
 | 
			
		||||
    {
 | 
			
		||||
        // load challenges
 | 
			
		||||
        $this->loadChallenges();
 | 
			
		||||
 | 
			
		||||
        // populate CSV file
 | 
			
		||||
        foreach ($this->challenges as $ch) {
 | 
			
		||||
            $csvline = [
 | 
			
		||||
                $ch["question"],
 | 
			
		||||
                $ch["image_url"],
 | 
			
		||||
            ];
 | 
			
		||||
            $csvline = array_merge($csvline, $ch["answers"]);
 | 
			
		||||
            fputcsv($f, $csvline);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get game directory NAME with path. Does not check if the game directory exists or not.
 | 
			
		||||
    function getGameDir(): string
 | 
			
		||||
    {
 | 
			
		||||
        return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get game file NAME with path. Does not check whether the game file is in place or not.
 | 
			
		||||
    function getGameFile(): string
 | 
			
		||||
    {
 | 
			
		||||
        return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId() . DIRECTORY_SEPARATOR . GAME_FILE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Is the given user the owner of the game?
 | 
			
		||||
    function isUserOwner(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->owner === $nickname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Is the given user a contributor of the game?
 | 
			
		||||
    function isUserContributor(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($nickname, $this->contributors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Is user contributor or owner?
 | 
			
		||||
    function isUserContributorOrOwner(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->isUserContributor($nickname) || $this->isUserOwner($nickname);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const CSV_ENCODINGS = ["UTF-8", "Windows-1252"];
 | 
			
		||||
 | 
			
		||||
    // Import challenges from a CSV table.
 | 
			
		||||
    function importChallengesFromCSV(string $csv_path): array
 | 
			
		||||
    {
 | 
			
		||||
        // convert text encoding into UTF-8
 | 
			
		||||
        $data = file_get_contents($csv_path);
 | 
			
		||||
        $encoding = "UNKNOWN";
 | 
			
		||||
        foreach (self::CSV_ENCODINGS as $enc) { // detect encoding
 | 
			
		||||
            if (mb_check_encoding($data, $enc)) {
 | 
			
		||||
                $encoding = $enc;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($encoding !== "UNKNOWN") { // if encoding has been detected successfully
 | 
			
		||||
            $data = mb_convert_encoding($data, "UTF-8", $encoding);
 | 
			
		||||
            file_put_contents($csv_path, $data);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // clear challenges
 | 
			
		||||
        $this->challenges = [];
 | 
			
		||||
 | 
			
		||||
        // load filled CSV file
 | 
			
		||||
        $f = fopen($csv_path, "r");
 | 
			
		||||
        if (!$f) { // failed to open file
 | 
			
		||||
            return ["n" => 0, "encoding" => $encoding];
 | 
			
		||||
        }
 | 
			
		||||
        while ($csvline = fgetcsv($f)) {
 | 
			
		||||
            // skip empty lines
 | 
			
		||||
            if (trim(implode("", $csvline)) === "") {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            if (count($csvline) >= 3) {
 | 
			
		||||
                // construct challenge record
 | 
			
		||||
                $ch = [
 | 
			
		||||
                    "question" => trim($csvline[0]),
 | 
			
		||||
                    "image_url" => trim($csvline[1]),
 | 
			
		||||
                    "correct_answer" => trim($csvline[2]),
 | 
			
		||||
                    "answers" => array_filter(array_slice($csvline, 2), function ($v) {
 | 
			
		||||
                        return trim($v) !== "";
 | 
			
		||||
                    })
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
                // if image is attached to the challenge, then give a random name to the image
 | 
			
		||||
                if ($ch["image_url"] !== "") {
 | 
			
		||||
                    $old_img_name = $ch["image_url"];
 | 
			
		||||
                    $ext = pathinfo($old_img_name, PATHINFO_EXTENSION);
 | 
			
		||||
                    $ext = ($ext !== "") ? ("." . $ext) : $ext;
 | 
			
		||||
                    $new_img_name = uniqid("img_", true) . $ext;
 | 
			
		||||
                    $ch["image_url"] = $new_img_name;
 | 
			
		||||
 | 
			
		||||
                    // rename the actual file
 | 
			
		||||
                    $old_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $old_img_name;
 | 
			
		||||
                    $new_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $new_img_name;
 | 
			
		||||
                    rename($old_img_path, $new_img_path);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // store the challenge
 | 
			
		||||
                $this->challenges[] = $ch;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        fclose($f);
 | 
			
		||||
 | 
			
		||||
        // save challenges
 | 
			
		||||
        $this->saveChallenges();
 | 
			
		||||
 | 
			
		||||
        // update game with game file present
 | 
			
		||||
        $this->gameFileIsPresent = true;
 | 
			
		||||
 | 
			
		||||
        // store modifications
 | 
			
		||||
        $this->commitMods();
 | 
			
		||||
 | 
			
		||||
        return ["n" => count($this->challenges), "encoding" => $encoding];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ---------
 | 
			
		||||
 | 
			
		||||
    public function getName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setName(string $name): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getOwner(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->owner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setOwner(string $owner): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->owner = $owner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getContributors(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->contributors;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setContributors(array $contributors): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->contributors = $contributors;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setDescription(string $description): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->description = $description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isGameFileIsPresent(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->gameFileIsPresent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setProperties(array $properties): void {
 | 
			
		||||
        $this->properties = $properties;
 | 
			
		||||
        $this->commitMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function& getProperties(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->properties;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isPublic(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->public;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getPublicId(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->publicId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setPublic(bool $public) : void {
 | 
			
		||||
        $this->public = $public;
 | 
			
		||||
        $this->commitMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getChallenges(): array
 | 
			
		||||
    {
 | 
			
		||||
        $this->loadChallenges();
 | 
			
		||||
        return $this->challenges;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class GameMgr
 | 
			
		||||
{
 | 
			
		||||
@ -39,7 +376,7 @@ class GameMgr
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addGame(string $name, string $owner, string $description, array $properties = Game::DEFAULT_GAME_PROPERTIES,
 | 
			
		||||
                     array  $contributors = [], array $tasks = []): bool
 | 
			
		||||
                     array  $contributors = [], array $challenges = []): bool
 | 
			
		||||
    {
 | 
			
		||||
        $game_data = [
 | 
			
		||||
            "name" => $name,
 | 
			
		||||
@ -59,7 +396,7 @@ class GameMgr
 | 
			
		||||
        $game = Game::fromArray($this, $game_data);
 | 
			
		||||
        $current_game_media_dir = $game->getGameDir();
 | 
			
		||||
        mkdir($current_game_media_dir);
 | 
			
		||||
        $game->saveTasks();
 | 
			
		||||
        $game->saveChallenges();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -122,22 +459,5 @@ class GameMgr
 | 
			
		||||
        $gameIds = array_map(fn($r) => $r["name"] . "#" . $r["_id"] ,$a);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function upgradeGames(array $ids = []): void
 | 
			
		||||
    {
 | 
			
		||||
        $a = [];
 | 
			
		||||
        if ($ids === []) {
 | 
			
		||||
            $a = $this->db->findAll();
 | 
			
		||||
        } else {
 | 
			
		||||
            $a = $this->db->findBy(["_id", "IN", $ids]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($a as $g) {
 | 
			
		||||
            $game = Game::fromArray($this, $g);
 | 
			
		||||
            $game->loadTasks();
 | 
			
		||||
            $game->saveTasks();
 | 
			
		||||
            $game->storeMods();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										226
									
								
								class/Group.php
									
									
									
									
									
								
							
							
						
						
									
										226
									
								
								class/Group.php
									
									
									
									
									
								
							@ -1,226 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "AutoStoring.php";
 | 
			
		||||
 | 
			
		||||
class Group extends AutoStoring
 | 
			
		||||
{
 | 
			
		||||
    private int $_id; // Group's ID (assigned by SleekDB)
 | 
			
		||||
    private string $name; // Group's name
 | 
			
		||||
    private bool $unique; // Indicates if name is unique or not
 | 
			
		||||
    private string $owner; // Group owner's nickname
 | 
			
		||||
    private string $description; // Group description
 | 
			
		||||
    private array $editors; // Nicknames of users able to manage the group
 | 
			
		||||
    private array $members; // Nickname of group members
 | 
			
		||||
    private array $games; // Game IDs assigned to this group
 | 
			
		||||
    private GroupMgr $groupMgr; // Reference to GroupMgr object managing this group
 | 
			
		||||
 | 
			
		||||
    // --------------
 | 
			
		||||
 | 
			
		||||
    // store modifications to the database
 | 
			
		||||
    public function storeMods() : void
 | 
			
		||||
    {
 | 
			
		||||
        $this->groupMgr->updateGroup($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // --------------
 | 
			
		||||
 | 
			
		||||
    function __construct(GroupMgr &$groupMgr, string $name, string $description, string $owner, int $id = -1, bool $unique = true, array $editors = [], array $members = [], array $games = [])
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->_id = $id;
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->unique = $unique;
 | 
			
		||||
        $this->description = $description;
 | 
			
		||||
        $this->owner = $owner;
 | 
			
		||||
        $this->editors = $editors;
 | 
			
		||||
        $this->members = $members;
 | 
			
		||||
        $this->games = $games;
 | 
			
		||||
        $this->groupMgr = &$groupMgr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create Group from array
 | 
			
		||||
    static function fromArray(GroupMgr &$groupMgr, array $a): Group
 | 
			
		||||
    {
 | 
			
		||||
        $id = $a["_id"] ?? -1;
 | 
			
		||||
        return new Group($groupMgr, $a["groupname"], $a["description"], $a["owner"], $id, $a["unique"], $a["editors"], $a["users"], $a["games"]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convert Group to array
 | 
			
		||||
    function toArray(array $omit = []): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = [
 | 
			
		||||
            "_id" => $this->_id,
 | 
			
		||||
            "groupname" => $this->name,
 | 
			
		||||
            "unique" => $this->unique,
 | 
			
		||||
            "description" => $this->description,
 | 
			
		||||
            "owner" => $this->owner,
 | 
			
		||||
            "editors" => $this->editors,
 | 
			
		||||
            "users" => $this->members,
 | 
			
		||||
            "games" => $this->games
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        foreach ($omit as $field) {
 | 
			
		||||
            unset($a[$field]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group's ID.
 | 
			
		||||
    function getID(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->_id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group's name.
 | 
			
		||||
    function getName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set group's name.
 | 
			
		||||
    function setName(string $name): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Tell if group is unique
 | 
			
		||||
    function isUnique(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->unique;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group's description.
 | 
			
		||||
    function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set group's description.
 | 
			
		||||
    function setDescription(string $description): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->description = $description;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group's owner.
 | 
			
		||||
    function getOwner(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->owner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set group's owner.
 | 
			
		||||
    function setOwner(string $owner): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->owner = $owner;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get list of editors.
 | 
			
		||||
    function getEditors(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->editors;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set editors.
 | 
			
		||||
    function setEditors(array $editors): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->editors = $editors;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group members.
 | 
			
		||||
    function getMembers(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->members;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set group members.
 | 
			
		||||
    function setMembers(array $members): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->members = $members;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get games.
 | 
			
		||||
    function getGames(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->games;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set games.
 | 
			
		||||
    function setGames(array $games): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->games = $games;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Include/exclude members.
 | 
			
		||||
    function changeMembers(array $nicknames_add, array $nicknames_remove): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($nicknames_add as $nickname) { // add members
 | 
			
		||||
            alter_array_contents($this->members, $nickname, null);
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($nicknames_remove as $nickname) { // remove members
 | 
			
		||||
            alter_array_contents($this->members, null, $nickname); // delete from members
 | 
			
		||||
            alter_array_contents($this->editors, null, $nickname); // delete from editors
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->storeMods(); // store changes
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add members
 | 
			
		||||
    function addMembers(array $nicknames) : void {
 | 
			
		||||
        $this->changeMembers($nicknames, []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove members
 | 
			
		||||
    function removeMembers(array $nicknames) : void {
 | 
			
		||||
        $this->changeMembers([], $nicknames);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Include/exclude games.
 | 
			
		||||
    function changeGames(array $gameids_add, array $gameids_remove): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($gameids_add as $gameid) { // add games
 | 
			
		||||
            alter_array_contents($this->games, $gameid, null);
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($gameids_remove as $gameid) { // remove games
 | 
			
		||||
            alter_array_contents($this->games, null, $gameid);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->storeMods(); // store changes
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Returns whether the user is an editor of this group.
 | 
			
		||||
    function isUserEditor(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($nickname, $this->editors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Returns whether the user is an editor or the owner of the group.
 | 
			
		||||
    function isUserContributor(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->isUserEditor($nickname) || ($this->owner === $nickname);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Returns if user is member of the group.
 | 
			
		||||
    function isMember(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($nickname, $this->members);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Return if game is assigned to this group.
 | 
			
		||||
    function isGameAssigned(string $gameid): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($gameid, $this->games);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get groups unique name.
 | 
			
		||||
    function getUniqueName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name . ($this->unique ? "" : ("#" . $this->_id));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -6,7 +6,228 @@ require_once "AutoStoring.php";
 | 
			
		||||
 | 
			
		||||
require_once "privilege_levels.php";
 | 
			
		||||
 | 
			
		||||
require_once "Group.php";
 | 
			
		||||
class Group extends AutoStoring
 | 
			
		||||
{
 | 
			
		||||
    private int $_id; // Group's ID (assigned by SleekDB)
 | 
			
		||||
    private string $name; // Group's name
 | 
			
		||||
    private bool $unique; // Indicates if name is unique or not
 | 
			
		||||
    private string $owner; // Group owner's nickname
 | 
			
		||||
    private string $description; // Group description
 | 
			
		||||
    private array $editors; // Nicknames of users able to manage the group
 | 
			
		||||
    private array $members; // Nickname of group members
 | 
			
		||||
    private array $games; // Game IDs assigned to this group
 | 
			
		||||
    private GroupMgr $groupMgr; // Reference to GroupMgr object managing this group
 | 
			
		||||
 | 
			
		||||
    // --------------
 | 
			
		||||
 | 
			
		||||
    // store modifications to the database
 | 
			
		||||
    public function storeMods() : void
 | 
			
		||||
    {
 | 
			
		||||
        $this->groupMgr->updateGroup($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // --------------
 | 
			
		||||
 | 
			
		||||
    function __construct(GroupMgr &$groupMgr, string $name, string $description, string $owner, int $id = -1, bool $unique = true, array $editors = [], array $members = [], array $games = [])
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->_id = $id;
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->unique = $unique;
 | 
			
		||||
        $this->description = $description;
 | 
			
		||||
        $this->owner = $owner;
 | 
			
		||||
        $this->editors = $editors;
 | 
			
		||||
        $this->members = $members;
 | 
			
		||||
        $this->games = $games;
 | 
			
		||||
        $this->groupMgr = &$groupMgr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create Group from array
 | 
			
		||||
    static function fromArray(GroupMgr &$groupMgr, array $a): Group
 | 
			
		||||
    {
 | 
			
		||||
        $id = $a["_id"] ?? -1;
 | 
			
		||||
        return new Group($groupMgr, $a["groupname"], $a["description"], $a["owner"], $id, $a["unique"], $a["editors"], $a["users"], $a["games"]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convert Group to array
 | 
			
		||||
    function toArray(array $omit = []): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = [
 | 
			
		||||
            "_id" => $this->_id,
 | 
			
		||||
            "groupname" => $this->name,
 | 
			
		||||
            "unique" => $this->unique,
 | 
			
		||||
            "description" => $this->description,
 | 
			
		||||
            "owner" => $this->owner,
 | 
			
		||||
            "editors" => $this->editors,
 | 
			
		||||
            "users" => $this->members,
 | 
			
		||||
            "games" => $this->games
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        foreach ($omit as $field) {
 | 
			
		||||
            unset($a[$field]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group's ID.
 | 
			
		||||
    function getID(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->_id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group's name.
 | 
			
		||||
    function getName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set group's name.
 | 
			
		||||
    function setName(string $name): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->name = $name;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Tell if group is unique
 | 
			
		||||
    function isUnique(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->unique;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group's description.
 | 
			
		||||
    function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->description;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set group's description.
 | 
			
		||||
    function setDescription(string $description): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->description = $description;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group's owner.
 | 
			
		||||
    function getOwner(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->owner;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set group's owner.
 | 
			
		||||
    function setOwner(string $owner): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->owner = $owner;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get list of editors.
 | 
			
		||||
    function getEditors(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->editors;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set editors.
 | 
			
		||||
    function setEditors(array $editors): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->editors = $editors;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get group members.
 | 
			
		||||
    function getMembers(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->members;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set group members.
 | 
			
		||||
    function setMembers(array $members): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->members = $members;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get games.
 | 
			
		||||
    function getGames(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->games;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set games.
 | 
			
		||||
    function setGames(array $games): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->games = $games;
 | 
			
		||||
        $this->storeMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Include/exclude members.
 | 
			
		||||
    function changeMembers(array $nicknames_add, array $nicknames_remove): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($nicknames_add as $nickname) { // add members
 | 
			
		||||
            alter_array_contents($this->members, $nickname, null);
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($nicknames_remove as $nickname) { // remove members
 | 
			
		||||
            alter_array_contents($this->members, null, $nickname); // delete from members
 | 
			
		||||
            alter_array_contents($this->editors, null, $nickname); // delete from editors
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->storeMods(); // store changes
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add members
 | 
			
		||||
    function addMembers(array $nicknames) : void {
 | 
			
		||||
        $this->changeMembers($nicknames, []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove members
 | 
			
		||||
    function removeMembers(array $nicknames) : void {
 | 
			
		||||
        $this->changeMembers([], $nicknames);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Include/exclude games.
 | 
			
		||||
    function changeGames(array $gameids_add, array $gameids_remove): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($gameids_add as $gameid) { // add games
 | 
			
		||||
            alter_array_contents($this->games, $gameid, null);
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($gameids_remove as $gameid) { // remove games
 | 
			
		||||
            alter_array_contents($this->games, null, $gameid);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->storeMods(); // store changes
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Returns whether the user is an editor of this group.
 | 
			
		||||
    function isUserEditor(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($nickname, $this->editors);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Returns whether the user is an editor or the owner of the group.
 | 
			
		||||
    function isUserContributor(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->isUserEditor($nickname) || ($this->owner === $nickname);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Returns if user is member of the group.
 | 
			
		||||
    function isMember(string $nickname): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($nickname, $this->members);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Return if game is assigned to this group.
 | 
			
		||||
    function isGameAssigned(string $gameid): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($gameid, $this->games);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get groups unique name.
 | 
			
		||||
    function getUniqueName(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->name . ($this->unique ? "" : ("#" . $this->_id));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class GroupMgr
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
@ -1,271 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
 | 
			
		||||
 | 
			
		||||
require_once "PythonUtils.php";
 | 
			
		||||
 | 
			
		||||
class LogicFunction implements JsonSerializable
 | 
			
		||||
{
 | 
			
		||||
    private static ExpressionLanguage $EXP_LANG; // expression language for linting an evaluation
 | 
			
		||||
    private array $input_vars; // array of input variables
 | 
			
		||||
    private string $expression; // the logic function in the bitwise Verilog-form
 | 
			
		||||
 | 
			
		||||
    public static function convertToVerilogBitwiseForm(string $expression): string
 | 
			
		||||
    {
 | 
			
		||||
        return str_replace(["/", "!", "*", "+"], ["~", "~", "&", "|"], $expression);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function adaptToEL(string $expression): string
 | 
			
		||||
    {
 | 
			
		||||
        return str_replace(["~"], [" not "], $expression);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function collectVariables(string $expression): array
 | 
			
		||||
    {
 | 
			
		||||
        preg_match_all("/\w/", $expression, $variables);
 | 
			
		||||
        return array_filter(array_unique($variables[0]), fn($v) => !empty($v) && !is_numeric($v[0]));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function __construct(string $expression = "", array $input_vars = [])
 | 
			
		||||
    {
 | 
			
		||||
        $this->setExpression($expression);
 | 
			
		||||
        $this->setInputVars(($input_vars === []) ? self::collectVariables($this->expression) : $input_vars);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTruthTable(): string
 | 
			
		||||
    {
 | 
			
		||||
        $N = count($this->input_vars);
 | 
			
		||||
        if ($N == 0) {
 | 
			
		||||
            return "";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $M = pow(2, $N);
 | 
			
		||||
 | 
			
		||||
        $vars = [];
 | 
			
		||||
        foreach ($this->input_vars as $var) {
 | 
			
		||||
            $vars[$var] = 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $expression = self::adaptToEL($this->getExpression("verilog_logic"));
 | 
			
		||||
//        printf("Cooked: %s\n", $cooked_form);
 | 
			
		||||
 | 
			
		||||
        $tt = [];
 | 
			
		||||
        for ($i = 0; $i < $M; $i++) {
 | 
			
		||||
            for ($k = 0; $k < $N; $k++) {
 | 
			
		||||
                $vars[$this->input_vars[$k]] = (($i >> ($N - $k - 1)) & 1) === 1;
 | 
			
		||||
//                printf("%d ", $vars[$this->input_vars[$k]]);
 | 
			
		||||
            }
 | 
			
		||||
            $out = self::$EXP_LANG->evaluate($expression, $vars);
 | 
			
		||||
            $out_str = $out ? "1" : "0";
 | 
			
		||||
//            printf("%d\n", $out);
 | 
			
		||||
            $tt[] = $out_str;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return join("", $tt);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function genTerm(array $vars, int $ftn, int $tn, int $mind, int $maxd, bool $top = true, int $opindex = 1): string
 | 
			
		||||
    {
 | 
			
		||||
        $term = "";
 | 
			
		||||
        $m = max($ftn, random_int(1, $tn));
 | 
			
		||||
        if ((($maxd === 0) || ($m === 1)) && ($ftn === 0)) {
 | 
			
		||||
            $neg = random_int(0, 1) === 1;
 | 
			
		||||
            $var = $vars[array_rand($vars, 1)];
 | 
			
		||||
            $term = ($neg ? "~" : "") . $var;
 | 
			
		||||
        } else {
 | 
			
		||||
            $depth = random_int(0, max(0, $maxd - 1));
 | 
			
		||||
 | 
			
		||||
            $verilog_ops = [" & ", " | "];
 | 
			
		||||
            $verilog_op = $verilog_ops[$opindex];
 | 
			
		||||
            $term = !$top ? "(" : "";
 | 
			
		||||
 | 
			
		||||
            $nextopindex = ($opindex === 0) ? 1 : 0;
 | 
			
		||||
 | 
			
		||||
            for ($i = 0; $i < $m; $i++) {
 | 
			
		||||
                $subterm = self::genTerm($vars, (($mind - 1) > 0) ? $ftn : 0, $tn, $mind - 1, $depth, false, $nextopindex);
 | 
			
		||||
                $term .= $subterm;
 | 
			
		||||
                if ($i < $m - 1) {
 | 
			
		||||
                    $term .= $verilog_op;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            $term .= !$top ? ")" : "";
 | 
			
		||||
        }
 | 
			
		||||
        return $term;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function genRandom(array $input_vars, int $min_depth = 2, int $max_depth = 3): LogicFunction
 | 
			
		||||
    {
 | 
			
		||||
        $term = self::genTerm($input_vars, count($input_vars), count($input_vars), $min_depth, $max_depth);
 | 
			
		||||
        return new LogicFunction($term, $input_vars);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function genRandomDNF($input_vars): LogicFunction
 | 
			
		||||
    {
 | 
			
		||||
        $N = count($input_vars);
 | 
			
		||||
        $states = pow(2, $N);
 | 
			
		||||
 | 
			
		||||
        $verilog_term = "";
 | 
			
		||||
        for ($i = 0; $i < $states; $i++) {
 | 
			
		||||
            $inside = "";
 | 
			
		||||
            $omit = random_int(0, 1); // omit the term or not?
 | 
			
		||||
            if (!$omit) {
 | 
			
		||||
                for ($j = 0; $j < $N; $j++) {
 | 
			
		||||
                    $neg = !($i & (1 << $j)); // is it an inverted variable?
 | 
			
		||||
                    $term = $input_vars[$j];
 | 
			
		||||
                    if ($neg) {
 | 
			
		||||
                        $inside .= "~" . $term;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        $inside .= $term;
 | 
			
		||||
                    }
 | 
			
		||||
                    if ($j < ($N - 1)) {
 | 
			
		||||
                        $inside .= " & ";
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($inside !== "") {
 | 
			
		||||
                $verilog_term .= "(";
 | 
			
		||||
                $verilog_term .= $inside;
 | 
			
		||||
                $verilog_term .= ")";
 | 
			
		||||
                if (($i < ($states - 1)) && !$omit) {
 | 
			
		||||
                    $verilog_term .= " | ";
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $verilog_term = rtrim($verilog_term, "| ");
 | 
			
		||||
        return new LogicFunction($verilog_term, $input_vars);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setExpression(string $expression): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->expression = self::convertToVerilogBitwiseForm($expression);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getExpression(string $fmt = "verilog_bitwise"): string
 | 
			
		||||
    {
 | 
			
		||||
        switch (strtolower($fmt)) {
 | 
			
		||||
            case "verilog_logic":
 | 
			
		||||
                return str_replace(["&", "|", "~"], ["&&", "||", "!"], $this->expression);
 | 
			
		||||
            case "tex":
 | 
			
		||||
            {
 | 
			
		||||
                $tex_form = str_replace([" | ", " & ", "(", ")"], [" + ", " \\cdot ", "\\left(", "\\right)"], $this->expression);
 | 
			
		||||
                return preg_replace("/~([a-zA-Z0-9_])/", '\\overline{$1}', $tex_form);
 | 
			
		||||
            }
 | 
			
		||||
            default:
 | 
			
		||||
            case "verilog_bitwise":
 | 
			
		||||
                return $this->expression;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getInputVars(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->input_vars;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setInputVars(array $input_vars): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->input_vars = $input_vars;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getNStates(): int
 | 
			
		||||
    {
 | 
			
		||||
        return pow(2, count($this->input_vars));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isValid(): bool
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            self::$EXP_LANG->lint(self::adaptToEL($this->expression), $this->input_vars);
 | 
			
		||||
        } catch (Exception $e) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function toArray(): array
 | 
			
		||||
    {
 | 
			
		||||
        return ["expression" => $this->expression, "input_vars" => $this->input_vars];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function jsonSerialize()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function fromArray(array $a): LogicFunction
 | 
			
		||||
    {
 | 
			
		||||
        return new LogicFunction($a["expression"] ?? "", $a["input_vars"] ?? []);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // general minterm regex: ([/!~]*[a-zA-Z_][a-zA-Z0-9_]*[&]{1,2})*([/!~]*[a-zA-Z_][a-zA-Z0-9_]*)
 | 
			
		||||
    // specific regex: ([\/!~]*(<input vars>)[&]{1,2})*([\/!~]*(<input vars>))
 | 
			
		||||
    public static function isCorrectDNF(array $input_vars, string $exp): bool
 | 
			
		||||
    {
 | 
			
		||||
        $exp = trim($exp); // trim spaces
 | 
			
		||||
        if ($exp === "0") {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        $minterms = explode("|", $exp); // break up the expression into minterms
 | 
			
		||||
        $minterms = array_map(fn($mt) => trim($mt, " ()\t"), $minterms); // strip the parentheses off the minterms
 | 
			
		||||
        $minterms = array_map(fn($mt) => str_replace(" ", "", $mt), $minterms); // remove spaces
 | 
			
		||||
        $ivars = implode("|", $input_vars); // create | separated list of input vars to be used with the regular expression
 | 
			
		||||
        $regex = "/([\/!~]*(${ivars})[&]{1,2})*([\/!~]*(${ivars}))/"; // specific regular expression
 | 
			
		||||
        foreach ($minterms as $minterm) {
 | 
			
		||||
            if (preg_match($regex, $minterm) !== 1) { // generally try to match the minterm
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            preg_match_all("/[\/!~]*(${ivars})[&]*/", $minterm, $matches); // fetch variables
 | 
			
		||||
            $vars = $matches[1] ?? [];
 | 
			
		||||
            sort($vars); // sort detected variables
 | 
			
		||||
            sort($input_vars); // sort input variables
 | 
			
		||||
            if ($vars !== $input_vars) { // ensure each variable occurs just once
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function initStatic(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::$EXP_LANG = new ExpressionLanguage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function toDNF(): string
 | 
			
		||||
    {
 | 
			
		||||
        $N = count($this->input_vars);
 | 
			
		||||
        $M = pow(2, count($this->input_vars));
 | 
			
		||||
        $tt = $this->getTruthTable();
 | 
			
		||||
 | 
			
		||||
        $minterms = [];
 | 
			
		||||
        for ($i = 0; $i < $M; $i++) {
 | 
			
		||||
            $r = $tt[$i];
 | 
			
		||||
            if ($r == "1") {
 | 
			
		||||
                $term = "(";
 | 
			
		||||
                for ($j = 0; $j < $N; $j++) {
 | 
			
		||||
                    $inv = (($i >> ($N - $j - 1)) & 1) ? "" : "~";
 | 
			
		||||
                    $term .= $inv . $this->input_vars[$j];
 | 
			
		||||
                    if ($j < ($N - 1)) {
 | 
			
		||||
                        $term .= " & ";
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                $term .= ")";
 | 
			
		||||
                $minterms[] = $term;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $dnf = join(" | ", $minterms);
 | 
			
		||||
        if ($dnf === "") {
 | 
			
		||||
            $dnf = "0";
 | 
			
		||||
        }
 | 
			
		||||
        return $dnf;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function drawNetwork(string $outvar = "f"): string
 | 
			
		||||
    {
 | 
			
		||||
        $expr = str_replace(["^"], [" xor "], $this->getExpression());
 | 
			
		||||
        return PythonUtils::execPy("draw_logic_network.py", [$expr, $outvar]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
LogicFunction::initStatic();
 | 
			
		||||
@ -1,57 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
class LogicUtils
 | 
			
		||||
{
 | 
			
		||||
    public static function extend(string $num, int $base, int $exnd, bool $comp): string
 | 
			
		||||
    {
 | 
			
		||||
        $fd = (int)(base_convert($num[0], $base, 10)); // get first digit as a number
 | 
			
		||||
        $extd = (string)(($comp && ($fd >= ($base / 2))) ? ($base - 1) : 0); // get the extension digit
 | 
			
		||||
        return str_pad((string)($num), $exnd, $extd, STR_PAD_LEFT); // extend to the left
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function complement(string $num, int $base): string
 | 
			
		||||
    {
 | 
			
		||||
        // convert to an integer
 | 
			
		||||
        $M = (int)(base_convert($num, $base, 10));
 | 
			
		||||
 | 
			
		||||
        // check if the highest digit is less than the half of the base, if not, add a zero prefix
 | 
			
		||||
        $fd = (int)(base_convert($num[0], $base, 10));
 | 
			
		||||
 | 
			
		||||
        // create the basis for forming the complement
 | 
			
		||||
        $H = (string)($base - 1);
 | 
			
		||||
        $K_str = (int)(str_repeat($H, strlen($num)));
 | 
			
		||||
        if ($fd >= ($base / 2)) { // if one more digit is needed...
 | 
			
		||||
            $K_str = $H . $K_str; // prepend with a zero digit
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // convert to integer
 | 
			
		||||
        $K = (int)(base_convert($K_str, $base, 10));
 | 
			
		||||
 | 
			
		||||
        // form the base's complement
 | 
			
		||||
        $C = $K - $M + 1;
 | 
			
		||||
 | 
			
		||||
        // convert to the final base
 | 
			
		||||
        return base_convert((string)$C, 10, $base);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function changeRepresentation(int $num, int $base, string $rep, int $digits): string {
 | 
			
		||||
        $neg = $num < 0; // store if the value is negative
 | 
			
		||||
        $numa_str = (string)(abs($num)); // create the absolute value as a string
 | 
			
		||||
        $numa_str = base_convert($numa_str, 10, $base); // convert to specific base
 | 
			
		||||
        if ($neg) {
 | 
			
		||||
            if ($rep === "s") {
 | 
			
		||||
                $numa_str = self::extend($numa_str, $base, $digits, false);
 | 
			
		||||
                $numa_str = "-" . $numa_str;
 | 
			
		||||
            } else if ($rep === "c") {
 | 
			
		||||
                $numa_str = self::complement($numa_str, $base);
 | 
			
		||||
                $numa_str = self::extend($numa_str, $base, $digits, true);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            $numa_str = self::extend($numa_str, $base, $digits, false);
 | 
			
		||||
            if (($rep === "c") && ($numa_str[0] >= ($base / 2))) {
 | 
			
		||||
                $numa_str = "0" . $numa_str;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return $numa_str;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,51 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "LogicFunction.php";
 | 
			
		||||
 | 
			
		||||
class LuaUtils
 | 
			
		||||
{
 | 
			
		||||
    public static function l2pA(array|null $la): array {
 | 
			
		||||
        if ($la === null) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $r = [];
 | 
			
		||||
        $i = 0;
 | 
			
		||||
        foreach ($la as $v) {
 | 
			
		||||
            $r[$i++] = $v;
 | 
			
		||||
        }
 | 
			
		||||
        return $r;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function p2lA(array|null $pa): array {
 | 
			
		||||
        if ($pa === null) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $r = [];
 | 
			
		||||
        $i = 1;
 | 
			
		||||
        foreach ($pa as $v) {
 | 
			
		||||
            $r[$i++] = $v;
 | 
			
		||||
        }
 | 
			
		||||
        return $r;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function registerLuaLibs(LuaSandbox &$sandbox): void {
 | 
			
		||||
        self::registerLogicUtils($sandbox);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function registerLogicUtils(LuaSandbox &$sandbox): void {
 | 
			
		||||
        $sandbox->registerLibrary("logic", [
 | 
			
		||||
            "extend" => fn() => [ call_user_func(array(LogicUtils::class, "extend"), ...func_get_args()) ],
 | 
			
		||||
            "complement" => fn() => [ call_user_func(array(LogicUtils::class, "complement"), ...func_get_args()) ],
 | 
			
		||||
            "changeRepresentation" => fn() => [ call_user_func(array(LogicUtils::class, "changeRepresentation"), ...func_get_args()) ],
 | 
			
		||||
            "genRandomLogicFunction" => fn($iv, $mind, $maxd) => [ call_user_func(array(LogicFunction::class, "genRandom"), self::l2pA($iv), $mind, $maxd)->getExpression() ],
 | 
			
		||||
            "genRandomLogicFunctionDNF" => fn($iv) => [ call_user_func(array(LogicFunction::class, "genRandomDNF"), self::l2pA($iv))->getExpression() ],
 | 
			
		||||
            "isLogicFunctionValid" => fn($expr) => [ (new LogicFunction($expr ?? "0"))->isValid() ],
 | 
			
		||||
            "isLogicFunctionADNF" => fn($iv, $expr) => [ call_user_func(array(LogicFunction::class, "isCorrectDNF"), self::l2pA($iv), $expr ?? "0") ],
 | 
			
		||||
            "collectVariablesFromLogicFunction" => fn($expr) => call_user_func(array(LogicFunction::class, "collectVariables"), $expr ?? ""),
 | 
			
		||||
            "convertLogicFunctionToTruthTable" => fn($expr, $iv) => [ (new LogicFunction($expr ?? "0", self::l2pA($iv)))->getTruthTable() ],
 | 
			
		||||
            "drawLogicFunction" => fn($expr, $iv, $ov) => [ (new LogicFunction($expr ?? "0", self::l2pA($iv)))->drawNetwork($ov) ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "Utils.php";
 | 
			
		||||
 | 
			
		||||
class PythonUtils
 | 
			
		||||
{
 | 
			
		||||
    private const VENV = Utils::WORKSPACE_DIR . DIRECTORY_SEPARATOR . "venv";
 | 
			
		||||
 | 
			
		||||
    public static function execPy(string $script, array $args): string
 | 
			
		||||
    {
 | 
			
		||||
        $venv = getcwd() . DIRECTORY_SEPARATOR . self::VENV; // compose full venv path
 | 
			
		||||
        $ws = getcwd() . DIRECTORY_SEPARATOR . Utils::WORKSPACE_DIR; // compose full workspace path
 | 
			
		||||
 | 
			
		||||
        //$source_cmd = "source " . $venv . DIRECTORY_SEPARATOR . "bin" . DIRECTORY_SEPARATOR . "activate";
 | 
			
		||||
        $flattened_args = join(" ", array_map(fn($arg) => "'$arg'", $args)); // prepare arguments for use on command line
 | 
			
		||||
        $python_cmd = "bash $ws" . DIRECTORY_SEPARATOR . "py_exec.sh \"$ws" . DIRECTORY_SEPARATOR . $script . "\" " . $flattened_args . " 2>&1";
 | 
			
		||||
        return shell_exec($python_cmd); // execute python script
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -79,7 +79,7 @@ class Answer
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class TaskReport
 | 
			
		||||
class Challenge
 | 
			
		||||
{
 | 
			
		||||
    private string $question;
 | 
			
		||||
    private array $answers;
 | 
			
		||||
@ -143,21 +143,21 @@ class TaskReport
 | 
			
		||||
class ReportSection
 | 
			
		||||
{
 | 
			
		||||
    private string $title;
 | 
			
		||||
    private array $tasks;
 | 
			
		||||
    private array $challenges;
 | 
			
		||||
 | 
			
		||||
    private function getNumberOfSubmissions() : int {
 | 
			
		||||
        return count($this->tasks) > 0 ? $this->tasks[0]->getSubmissionCount() : 0;
 | 
			
		||||
        return count($this->challenges) > 0 ? $this->challenges[0]->getSubmissionCount() : 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function __construct(string $title, array $tasks)
 | 
			
		||||
    function __construct(string $title, array $challenges)
 | 
			
		||||
    {
 | 
			
		||||
        $this->title = $title;
 | 
			
		||||
        $this->tasks = array_map(fn($ch) => new TaskReport($ch), $tasks);
 | 
			
		||||
        $this->challenges = array_map(fn($ch) => new Challenge($ch), $challenges);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getTasks(): array
 | 
			
		||||
    function getChallenges(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->tasks;
 | 
			
		||||
        return $this->challenges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getTitle(): string
 | 
			
		||||
@ -169,8 +169,8 @@ class ReportSection
 | 
			
		||||
    function genTeX(): string
 | 
			
		||||
    {
 | 
			
		||||
        $tex = "\\begin{quiz}{" . $this->title . "}{" . $this->getNumberOfSubmissions() . "}\n";
 | 
			
		||||
        foreach ($this->tasks as $task) {
 | 
			
		||||
            $tex .= $task->genTeX();
 | 
			
		||||
        foreach ($this->challenges as $challenge) {
 | 
			
		||||
            $tex .= $challenge->genTeX();
 | 
			
		||||
        }
 | 
			
		||||
        $tex .= "\\end{quiz}\n";
 | 
			
		||||
        return $tex;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										309
									
								
								class/Task.php
									
									
									
									
									
								
							
							
						
						
									
										309
									
								
								class/Task.php
									
									
									
									
									
								
							@ -1,309 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
class Task implements JsonSerializable
 | 
			
		||||
{
 | 
			
		||||
    private string $type; // task type
 | 
			
		||||
    private string $question; // the task title
 | 
			
		||||
    protected mixed $player_answer; // answer given by the player
 | 
			
		||||
    protected mixed $correct_answer; // correct answer
 | 
			
		||||
    private string $comment; // solution_explanation
 | 
			
		||||
    private float $max_mark; // maximum points that can be collected at this task
 | 
			
		||||
    private float $mark; // earned points
 | 
			
		||||
    private bool $is_template; // this task is a template
 | 
			
		||||
    private array $flags; // task flags
 | 
			
		||||
    private string $lua_script; // path to the corresponding Lua script
 | 
			
		||||
    private Game|Test|null $governor; // object that governs this task
 | 
			
		||||
    private LuaSandbox|null $lua_sandbox; // Lua sandbox, initially NULL
 | 
			
		||||
    private array $lua_params; // Lua script parameters
 | 
			
		||||
 | 
			
		||||
    // -------------
 | 
			
		||||
 | 
			
		||||
    protected function addLuaLibraries(): void
 | 
			
		||||
    {
 | 
			
		||||
        // register member methods
 | 
			
		||||
        $method_names = get_class_methods($this);
 | 
			
		||||
        $methods = [];
 | 
			
		||||
        foreach ($method_names as $method_name) {
 | 
			
		||||
            $methods[$method_name] = fn() => [call_user_func(array(&$this, $method_name), ...func_get_args())];
 | 
			
		||||
        }
 | 
			
		||||
        $this->lua_sandbox->registerLibrary("task", $methods);
 | 
			
		||||
 | 
			
		||||
        // register generic functionality
 | 
			
		||||
        $this->lua_sandbox->registerLibrary("php", [
 | 
			
		||||
            "print" => function ($str) {
 | 
			
		||||
                printf("%s\n", $str);
 | 
			
		||||
            },
 | 
			
		||||
            "replace" => function ($search, $replace, $str) {
 | 
			
		||||
                return [str_replace(LuaUtils::l2pA($search), LuaUtils::l2pA($replace), $str)];
 | 
			
		||||
            },
 | 
			
		||||
            "replace_field" => function ($field, $replacement, $str) {
 | 
			
		||||
                return [str_replace("{{" . $field . "}}", $replacement, $str)];
 | 
			
		||||
            },
 | 
			
		||||
            "trim" => function ($str, $chars = " \n\r\t\v\0") {
 | 
			
		||||
                return [trim($str, $chars)];
 | 
			
		||||
            },
 | 
			
		||||
            "ltrim" => function ($str, $chars = " \n\r\t\v\0") {
 | 
			
		||||
                return [ltrim($str, $chars)];
 | 
			
		||||
            },
 | 
			
		||||
            "rtrim" => function ($str, $chars = " \n\r\t\v\0") {
 | 
			
		||||
                return [rtrim($str, $chars)];
 | 
			
		||||
            },
 | 
			
		||||
            "starts_with" => function ($str, $start) {
 | 
			
		||||
                return [str_starts_with($str, $start)];
 | 
			
		||||
            },
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        LuaUtils::registerLuaLibs($this->lua_sandbox);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function createLuaSandbox(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->lua_sandbox === null) {
 | 
			
		||||
            $this->lua_sandbox = new LuaSandbox;
 | 
			
		||||
            $this->addLuaLibraries();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function luaCall(string $lua_function): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->createLuaSandbox();
 | 
			
		||||
        $implementation = file_get_contents($this->getGameDir() . DIRECTORY_SEPARATOR . $this->lua_script);
 | 
			
		||||
        $function_call = "$lua_function()";
 | 
			
		||||
        $joined_code = $implementation . "\n\n" . $function_call;
 | 
			
		||||
        $fn = $this->lua_sandbox->loadString($joined_code);
 | 
			
		||||
        $fn->call();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function __construct(string $type, array &$a = null)
 | 
			
		||||
    {
 | 
			
		||||
        $this->type = $type;
 | 
			
		||||
        $this->is_template = $a["is_template"] ?? false;
 | 
			
		||||
        $this->max_mark = $a["max_mark"] ?? 1.0;
 | 
			
		||||
        $this->mark = $a["mark"] ?? -1;
 | 
			
		||||
        $this->question = $a["question"] ?? "";
 | 
			
		||||
        $this->flags = $a["flags"] ?? [];
 | 
			
		||||
        $this->player_answer = $a["player_answer"] ?? null;
 | 
			
		||||
        $this->correct_answer = $a["correct_answer"] ?? null;
 | 
			
		||||
        $this->comment = $a["comment"] ?? "";
 | 
			
		||||
        $this->lua_script = $a["lua_script"] ?? "";
 | 
			
		||||
        $this->lua_params = $a["lua_params"] ?? [];
 | 
			
		||||
 | 
			
		||||
        $this->governor = null;
 | 
			
		||||
        $this->lua_sandbox = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setQuestion(string $question): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->question = $question;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getQuestion(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->question;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // save answer
 | 
			
		||||
    function saveAnswer(mixed $ans): bool
 | 
			
		||||
    {
 | 
			
		||||
        $this->player_answer = $ans;
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // clear answer
 | 
			
		||||
    function clearAnswer(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->player_answer = "";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // set task type
 | 
			
		||||
    function setType(string $type): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->type = $type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // get task 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 setMark(float $mark): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->mark = max($mark, 0.0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getMark(): float
 | 
			
		||||
    {
 | 
			
		||||
        return $this->mark;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function luaCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->luaCall("check");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function staticCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->mark = $this->max_mark;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function autoCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->lua_script !== "") {
 | 
			
		||||
            $this->luaCheck();
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->staticCheck();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function toArray(string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = [
 | 
			
		||||
            "type" => $this->type,
 | 
			
		||||
            "question" => $this->question,
 | 
			
		||||
            "max_mark" => $this->max_mark,
 | 
			
		||||
            "mark" => $this->mark,
 | 
			
		||||
            "correct_answer" => $this->correct_answer,
 | 
			
		||||
            "comment" => $this->comment,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        if ($mode === "all") {
 | 
			
		||||
            $a["is_template"] = $this->is_template;
 | 
			
		||||
            $a["flags"] = $this->flags;
 | 
			
		||||
            $a["lua_script"] = $this->lua_script;
 | 
			
		||||
            $a["lua_params"] = $this->lua_params;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$this->isTemplate()) {
 | 
			
		||||
            $a["player_answer"] = $this->player_answer;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function jsonSerialize(): mixed
 | 
			
		||||
    {
 | 
			
		||||
        return $this->toArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setTemplate(bool $is_template): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->is_template = $is_template;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function isTemplate(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->is_template;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getFlags(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->flags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setFlags(array $flags): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->flags = $flags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function hasFlag(string $flag): bool
 | 
			
		||||
    {
 | 
			
		||||
        return in_array($flag, $this->flags);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setLuaScript(string $lua_script): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->lua_script = $lua_script;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLuaScript(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->lua_script;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLuaParams(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->lua_params;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setLuaParams(array $lua_params): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->lua_params = $lua_params;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getPlayerAnswer(): mixed
 | 
			
		||||
    {
 | 
			
		||||
        return $this->player_answer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
//    /* FIXME: ez ugyanaz, mint a saveAnswer() */
 | 
			
		||||
//    function setPlayerAnswer(mixed $player_answer): void {
 | 
			
		||||
//        $this->player_answer = $player_answer;
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    function setCorrectAnswer(mixed $correct_answer): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->correct_answer = $correct_answer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getCorrectAnswer(): mixed
 | 
			
		||||
    {
 | 
			
		||||
        return $this->correct_answer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setComment(string $comment): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->comment = $comment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addCommentLine(string $cmtl): void {
 | 
			
		||||
        $this->comment .= $cmtl . "<br>";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getComment(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->comment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function luaRandomize(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->luaCall("randomize");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function randomize(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->lua_script !== "") {
 | 
			
		||||
            $this->luaRandomize();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setGovernor(Game|Test|null &$governor): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->governor =  &$governor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function &getGovernor(): Game|Test|null
 | 
			
		||||
    {
 | 
			
		||||
        return $this->governor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getGameDir(): string
 | 
			
		||||
    {
 | 
			
		||||
        $gov = $this->getGovernor();
 | 
			
		||||
        if ($gov == null) {
 | 
			
		||||
            return "";
 | 
			
		||||
        } else {
 | 
			
		||||
            return $gov->getGameDir();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,55 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "Tasks/SingleChoiceTask.php";
 | 
			
		||||
 | 
			
		||||
require_once "Tasks/OpenEndedTask.php";
 | 
			
		||||
 | 
			
		||||
require_once "Tasks/NumberConversionTask.php";
 | 
			
		||||
 | 
			
		||||
require_once "Tasks/TruthTableTask.php";
 | 
			
		||||
 | 
			
		||||
require_once "Tasks/VerilogTask.php";
 | 
			
		||||
 | 
			
		||||
require_once "Tasks/LogicFunctionTask.php";
 | 
			
		||||
 | 
			
		||||
class TaskFactory
 | 
			
		||||
{
 | 
			
		||||
    static function fromArray(array $a, Game|Test|null &$governor = null): Task|null
 | 
			
		||||
    {
 | 
			
		||||
        $type = $a["type"] ?? "singlechoice"; // if the type is missing, then it's a single choice task
 | 
			
		||||
        switch ($type) {
 | 
			
		||||
            case "singlechoice":
 | 
			
		||||
                $task = new SingleChoiceTask($a);
 | 
			
		||||
                break;
 | 
			
		||||
            case "openended":
 | 
			
		||||
                $task = new OpenEndedTask($a);
 | 
			
		||||
                break;
 | 
			
		||||
            case "numberconversion":
 | 
			
		||||
                $task = new NumberConversionTask($a);
 | 
			
		||||
                break;
 | 
			
		||||
            case "truthtable":
 | 
			
		||||
                $task = new TruthTableTask($a);
 | 
			
		||||
                break;
 | 
			
		||||
            case "logicfunction":
 | 
			
		||||
                $task = new LogicFunctionTask($a);
 | 
			
		||||
                break;
 | 
			
		||||
            case "verilog":
 | 
			
		||||
                $task = new VerilogTask($a);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $task->setGovernor($governor);
 | 
			
		||||
        return $task;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static function constructFromCollection(array $c, Game|Test|null &$governor = null): array
 | 
			
		||||
    {
 | 
			
		||||
        $chgs = [];
 | 
			
		||||
        foreach ($c as $ch) {
 | 
			
		||||
            $chgs[] = TaskFactory::fromArray($ch, $governor);
 | 
			
		||||
        }
 | 
			
		||||
        return $chgs;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,60 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "LogicTaskBase.php";
 | 
			
		||||
 | 
			
		||||
class LogicFunctionTask extends LogicTaskBase
 | 
			
		||||
{
 | 
			
		||||
    private float $missing_minterm_score = -1.0; // score for a missing minterm
 | 
			
		||||
    public function __construct(array $a = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct("logicfunction", $a);
 | 
			
		||||
 | 
			
		||||
        $this->missing_minterm_score = $a["missing_minterm_score"] ?? -1.0;
 | 
			
		||||
 | 
			
		||||
        if (!$this->hasFlag("dnf")) {
 | 
			
		||||
            $this->setCorrectAnswer($this->getLogicFunction()->getExpression());
 | 
			
		||||
        } else {
 | 
			
		||||
            $dnf = $this->getLogicFunction()->toDNF();
 | 
			
		||||
            $this->setCorrectAnswer($dnf);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function toArray(string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = parent::toArray($mode);
 | 
			
		||||
 | 
			
		||||
        if ($this->hasFlag("attachtruthtable")) {
 | 
			
		||||
            $a["truthtable"] = $this->getTruthtable();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($mode == "all") {
 | 
			
		||||
            $a["missing_minterm_score"] = $this->missing_minterm_score;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getMissingMintermScore(): float
 | 
			
		||||
    {
 | 
			
		||||
        return $this->missing_minterm_score;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setMissingMintermScore(float $missing_minterm_score): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->missing_minterm_score = $missing_minterm_score;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function staticCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        $mark = 0.0;
 | 
			
		||||
        $pa = $this->player_answer ?? "";
 | 
			
		||||
        $iv = $this->getLogicFunction()->getInputVars();
 | 
			
		||||
        $palf = new LogicFunction($pa, $iv);
 | 
			
		||||
        if ($palf->isValid() && ((!$this->hasFlag("dnf")) || LogicFunction::isCorrectDNF($iv, $pa))) { // if the function is valid AND (if enabled) it is a correct DNF
 | 
			
		||||
            $patt = $palf->getTruthTable();
 | 
			
		||||
            $errs = $this->getTTDiffCntToCA($patt);
 | 
			
		||||
            $mark = $this->getMaxMark() + $errs * $this->getMissingMintermScore();
 | 
			
		||||
        }
 | 
			
		||||
        $this->setMark($mark);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,107 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "PicturedTask.php";
 | 
			
		||||
 | 
			
		||||
require_once "class/LogicFunction.php";
 | 
			
		||||
 | 
			
		||||
require_once "class/Utils.php";
 | 
			
		||||
 | 
			
		||||
require_once "class/LuaUtils.php";
 | 
			
		||||
 | 
			
		||||
class LogicTaskBase extends PicturedTask
 | 
			
		||||
{
 | 
			
		||||
    private LogicFunction $lf; // logic function
 | 
			
		||||
    private string $output_variable; // output variable
 | 
			
		||||
 | 
			
		||||
    public function __construct(string $type, array $a = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($type, $a);
 | 
			
		||||
 | 
			
		||||
        if (isset($a["function"])) { // fetching from a JSON-stored object
 | 
			
		||||
            $this->lf = LogicFunction::fromArray($a["function"]);
 | 
			
		||||
        } else if (isset($a["expression"], $a["input_variables"])) { // building from the scratch
 | 
			
		||||
            $this->lf = new LogicFunction($a["expression"], $a["input_variables"]);
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->lf = new LogicFunction();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->output_variable = $a["output_variable"] ?? "f";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setOutputVariable(string $ovar): void {
 | 
			
		||||
        $this->output_variable = $ovar;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getOutputVariable(): string {
 | 
			
		||||
        return $this->output_variable;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setLogicFunction(LogicFunction $lf): void {
 | 
			
		||||
        $this->lf = $lf;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLogicFunction(): LogicFunction {
 | 
			
		||||
        return $this->lf;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected function getTTDiffCntToCA(string $ott): int {
 | 
			
		||||
        $cans_tt = $this->getTruthTable();
 | 
			
		||||
        $errs = 0;
 | 
			
		||||
        for ($i = 0; $i < $this->getLogicFunction()->getNStates(); $i++) {
 | 
			
		||||
            if (($ott[$i] ?? " ") != $cans_tt[$i]) {
 | 
			
		||||
                $errs++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return $errs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function toArray(string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = parent::toArray($mode);
 | 
			
		||||
 | 
			
		||||
        if ($mode === "all") {
 | 
			
		||||
            $a["function"] = $this->lf->toArray();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $a["output_variable"] = $this->output_variable;
 | 
			
		||||
        $a["input_variables"] = $this->lf->getInputVars();
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function randomize(): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::randomize();
 | 
			
		||||
 | 
			
		||||
        if ($this->hasFlag("drawnetwork")) {
 | 
			
		||||
            $this->setImageData($this->lf->drawNetwork($this->output_variable));
 | 
			
		||||
            $this->setImageType("html");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // ---- Lua specific ----
 | 
			
		||||
 | 
			
		||||
    public function generateRandomFunction(array $input_vars, int $min_depth, int $max_depth): void {
 | 
			
		||||
        $this->lf = LogicFunction::genRandom(LuaUtils::l2pA($input_vars), $min_depth, $max_depth);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function generateRandomDF(array $input_vars): void {
 | 
			
		||||
        $this->lf = LogicFunction::genRandomDNF(LuaUtils::l2pA($input_vars));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setLogicFunctionExpr(string $expr, array $input_vars = []): void {
 | 
			
		||||
        $this->lf = new LogicFunction($expr, LuaUtils::l2pA($input_vars));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLogicFunctionExpr(string $fmt = "verilog_bitwise"): string {
 | 
			
		||||
        return $this->lf->getExpression($fmt);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLogicFunctionDNF(): string {
 | 
			
		||||
        return $this->lf->toDNF();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTruthTable(): string {
 | 
			
		||||
        return $this->lf->getTruthTable();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,133 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "OpenEndedTask.php";
 | 
			
		||||
 | 
			
		||||
require_once "class/LogicUtils.php";
 | 
			
		||||
 | 
			
		||||
class NumberConversionTask extends OpenEndedTask
 | 
			
		||||
{
 | 
			
		||||
    protected string $instruction; // instruction word
 | 
			
		||||
    protected string $source; // source
 | 
			
		||||
 | 
			
		||||
    // runtime variables -----
 | 
			
		||||
    private int $src_base; // source number system
 | 
			
		||||
    private string $src_rep; // source representation
 | 
			
		||||
    private int $src_n_digits; // minimum number of digits in the source
 | 
			
		||||
    private int $dst_base; // destination number system
 | 
			
		||||
    private string $dst_rep; // destination representation
 | 
			
		||||
    private int $dst_n_digits; // number of digits in the destination
 | 
			
		||||
 | 
			
		||||
    // -------------------------
 | 
			
		||||
    public function __construct(array &$a = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($a);
 | 
			
		||||
 | 
			
		||||
        $this->setType("numberconversion");
 | 
			
		||||
 | 
			
		||||
        // get instruction word
 | 
			
		||||
        $this->instruction = strtolower(trim($a["instruction"] ?? "10u:2->2u:4"));
 | 
			
		||||
 | 
			
		||||
        // expand it
 | 
			
		||||
        $pattern = "/([0-9]+)([suc]):([0-9]+)->([0-9]+)([suc]):([0-9]+)/";
 | 
			
		||||
        preg_match($pattern, $this->instruction, $matches);
 | 
			
		||||
 | 
			
		||||
        // if the instruction was meaningful
 | 
			
		||||
        if (count($matches) == 7) {
 | 
			
		||||
            $this->src_base = (int)$matches[1];
 | 
			
		||||
            $this->src_rep = $matches[2];
 | 
			
		||||
            $this->src_n_digits = $matches[3];
 | 
			
		||||
            $this->dst_base = (int)$matches[4];
 | 
			
		||||
            $this->dst_rep = $matches[5];
 | 
			
		||||
            $this->dst_n_digits = $matches[6];
 | 
			
		||||
        } else { // no valid instruction word has been passed
 | 
			
		||||
            $this->src_base = 10;
 | 
			
		||||
            $this->src_rep = "u";
 | 
			
		||||
            $this->src_n_digits = 2;
 | 
			
		||||
            $this->dst_base = 2;
 | 
			
		||||
            $this->dst_rep = "u";
 | 
			
		||||
            $this->dst_n_digits = 4;
 | 
			
		||||
 | 
			
		||||
            $this->instruction = $this->src_base . $this->src_rep . ":" . $this->src_n_digits . "->" . $this->dst_base . $this->dst_rep . ":" . $this->dst_n_digits;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->source = $a["source"] ?? "---";
 | 
			
		||||
        $this->correct_answer = $a["correct_answer"] ?? "---";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function toArray(string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = parent::toArray($mode);
 | 
			
		||||
 | 
			
		||||
        $a["instruction"] = $this->instruction;
 | 
			
		||||
        $a["source"] = $this->source;
 | 
			
		||||
        $a["correct_answer"] = $this->correct_answer;
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function randomize(): void
 | 
			
		||||
    {
 | 
			
		||||
        parent::randomize();
 | 
			
		||||
 | 
			
		||||
        // validate representation marks
 | 
			
		||||
        $invalid = in_array($this->src_rep . $this->dst_rep, ["us", "su"]);
 | 
			
		||||
        if ($invalid) { // fix invalid representation pairs
 | 
			
		||||
            $this->dst_rep = "u";
 | 
			
		||||
            $this->src_rep = "u";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // switch between source and destination constrains
 | 
			
		||||
        if ($this->hasFlag("sourceconstrain")) {
 | 
			
		||||
            $base = $this->src_base;
 | 
			
		||||
            $n_digits = $this->src_n_digits;
 | 
			
		||||
            $rep = $this->src_rep;
 | 
			
		||||
        } else {
 | 
			
		||||
            $base = $this->dst_base;
 | 
			
		||||
            $n_digits = $this->dst_n_digits;
 | 
			
		||||
            $rep = $this->dst_rep;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // specify the range
 | 
			
		||||
        $max = 1;
 | 
			
		||||
        $min = 0;
 | 
			
		||||
        switch ($rep) {
 | 
			
		||||
            case "u":
 | 
			
		||||
                $max = pow($base, $n_digits) - 1;
 | 
			
		||||
                $min = 0;
 | 
			
		||||
                break;
 | 
			
		||||
            case "s":
 | 
			
		||||
                $max = pow($base, $n_digits) - 1;
 | 
			
		||||
                $min = -$max;
 | 
			
		||||
                break;
 | 
			
		||||
            case "c":
 | 
			
		||||
                $max = pow($base, $n_digits - 1) - 1;
 | 
			
		||||
                $min = -($max + 1);
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // randomize a value
 | 
			
		||||
        $m = random_int($min, $max);
 | 
			
		||||
 | 
			
		||||
        // create the question and the answer
 | 
			
		||||
        $this->correct_answer = LogicUtils::changeRepresentation($m, $this->dst_base, $this->dst_rep, $this->dst_n_digits);
 | 
			
		||||
        $this->source = LogicUtils::changeRepresentation($m, $this->src_base, $this->src_rep, $this->src_n_digits);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function staticCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        $mark = 0.0;
 | 
			
		||||
        $pa = strtolower($this->player_answer);
 | 
			
		||||
        $ca = $this->correct_answer;
 | 
			
		||||
        if (($ca[0] === "-") && ($pa[0] !== "-")) {
 | 
			
		||||
            goto setmark;
 | 
			
		||||
        }
 | 
			
		||||
        if ($this->hasFlag("acceptwithoutleadingzeros")) {
 | 
			
		||||
            $mark = (ltrim($pa, "+- 0") === ltrim($ca, "-0")) ? $this->getMaxMark() : 0.0;
 | 
			
		||||
        } else {
 | 
			
		||||
            $mark = (trim($pa) === trim($ca)) ? $this->getMaxMark() : 0.0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setmark:
 | 
			
		||||
        $this->setMark($mark);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,64 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "PicturedTask.php";
 | 
			
		||||
 | 
			
		||||
class OpenEndedTask extends PicturedTask
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(array $a = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct("openended", $a);
 | 
			
		||||
 | 
			
		||||
        $this->correct_answer = $a["correct_answer"] ?? [];
 | 
			
		||||
        $this->player_answer = $this->player_answer ?? "";
 | 
			
		||||
        $this->setMaxMark(1.0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addCorrectAnswer(string $ca): void {
 | 
			
		||||
        $this->correct_answer[] = $ca;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function clearCorrectAnswers(): void {
 | 
			
		||||
        $this->correct_answer = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function saveAnswer(mixed $ans): bool
 | 
			
		||||
    {
 | 
			
		||||
        // collect transformations
 | 
			
		||||
        $transform_fns = [];
 | 
			
		||||
        foreach ($this->getFlags() as $flag) {
 | 
			
		||||
            switch ($flag) {
 | 
			
		||||
                case "makeuppercase":
 | 
			
		||||
                    $transform_fns[] = "strtoupper";
 | 
			
		||||
                    break;
 | 
			
		||||
                case "makelowercase":
 | 
			
		||||
                    $transform_fns[] = "strtolower";
 | 
			
		||||
                    break;
 | 
			
		||||
                case "removespaces":
 | 
			
		||||
                    $transform_fns[] = fn($str) => str_replace(" ", "", $str);
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // run the transformations
 | 
			
		||||
        foreach ($transform_fns as $tfn) {
 | 
			
		||||
            $ans = $tfn($ans);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // save the answer
 | 
			
		||||
        $this->player_answer = $ans;
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function staticCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        $mark = in_array($this->player_answer, $this->correct_answer) ? $this->getMaxMark() : 0.0;
 | 
			
		||||
        $this->setMark($mark);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function toArray(string $mode = "all"): array {
 | 
			
		||||
        $a = parent::toArray($mode);
 | 
			
		||||
        $a["correct_answer"] = $this->correct_answer;
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,53 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "class/Task.php";
 | 
			
		||||
 | 
			
		||||
class PicturedTask extends Task
 | 
			
		||||
{
 | 
			
		||||
    private string $image_data; // image data or the URL
 | 
			
		||||
    private string $image_type; // the type of the image
 | 
			
		||||
 | 
			
		||||
    function __construct(string $type, array &$a = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($type, $a);
 | 
			
		||||
 | 
			
		||||
        $this->image_data = $a["image_data"] ?? ($a["image_url"] ?? "");
 | 
			
		||||
        $this->image_type = $a["image_type"] ?? "none";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setImageData(string $image_data): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->image_data = $image_data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getImageData(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->image_data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setImageType(string $image_type): void {
 | 
			
		||||
        $this->image_type = $image_type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getImageType(): string {
 | 
			
		||||
        return $this->image_type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setUrlData(string $url_data): void {
 | 
			
		||||
        $this->setImageData($url_data);
 | 
			
		||||
        $this->setImageType("url");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setHtmlData(string $svg_data): void {
 | 
			
		||||
        $this->setImageData($svg_data);
 | 
			
		||||
        $this->setImageType("html");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function toArray(string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = parent::toArray($mode);
 | 
			
		||||
        $a["image_data"] = $this->image_data;
 | 
			
		||||
        $a["image_type"] = $this->image_type;
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,92 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "PicturedTask.php";
 | 
			
		||||
 | 
			
		||||
class SingleChoiceTask extends PicturedTask
 | 
			
		||||
{
 | 
			
		||||
    private array $answers; // possible answers
 | 
			
		||||
 | 
			
		||||
    // -----------------
 | 
			
		||||
 | 
			
		||||
    function __construct(array $a = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct("singlechoice", $a);
 | 
			
		||||
 | 
			
		||||
        $this->answers = $a["answers"] ?? [];
 | 
			
		||||
        $ca  = $a["correct_answer"] ?? -1;
 | 
			
		||||
        if (gettype($ca) === "string") { // backward compatibility
 | 
			
		||||
            $this->correct_answer = array_search($a["correct_answer"], $this->answers);
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->correct_answer = (int)($ca);
 | 
			
		||||
        }
 | 
			
		||||
        $this->player_answer = $a["player_answer"] ?? -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addAnswer(string $answer): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->answers[] = $answer;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function clearAnswers(): void {
 | 
			
		||||
        $this->answers = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getAnswers(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->answers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function isAnswerIdInsideBounds(int $ansid): bool
 | 
			
		||||
    {
 | 
			
		||||
        return ($ansid >= 0) && ($ansid <= count($this->answers));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function saveAnswer(mixed $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(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->player_answer = -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function staticCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        $mark = ($this->player_answer == $this->correct_answer) ? $this->getMaxMark() : 0.0;
 | 
			
		||||
        $this->setMark($mark);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function toArray(string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = parent::toArray($mode);
 | 
			
		||||
        $a["answers"] = $this->answers;
 | 
			
		||||
        $a["correct_answer"] = $this->correct_answer;
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function randomize(): void{
 | 
			
		||||
        parent::randomize();
 | 
			
		||||
 | 
			
		||||
        $ordering = range(0, count($this->answers) - 1); // create an ordered range
 | 
			
		||||
        shuffle($ordering); // shuffle indices
 | 
			
		||||
        $shans = [];
 | 
			
		||||
        $ca_remapped = false; // avoid remapping the correct answer multiple times
 | 
			
		||||
        for ($i = 0; $i < count($this->answers); $i++) { // shuffle answers
 | 
			
		||||
            $orig_pos = $ordering[$i];
 | 
			
		||||
            $shans[] = $this->answers[$orig_pos];
 | 
			
		||||
            if ((!$ca_remapped) && ($orig_pos == $this->correct_answer)) {
 | 
			
		||||
                $this->correct_answer = $i;
 | 
			
		||||
                $ca_remapped = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
        $this->answers = $shans;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,42 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "LogicTaskBase.php";
 | 
			
		||||
 | 
			
		||||
class TruthTableTask extends LogicTaskBase
 | 
			
		||||
{
 | 
			
		||||
    private float $bad_line_score; // points for a bad line in the truth table
 | 
			
		||||
    public function __construct(array $a = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct("truthtable", $a);
 | 
			
		||||
 | 
			
		||||
        $this->bad_line_score = $a["bad_line_score"] ?? -1.0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setBadLineScore(float $bad_line_score): void {
 | 
			
		||||
        $this->bad_line_score = $bad_line_score;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getBadLineScore(): float {
 | 
			
		||||
        return $this->bad_line_score;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function staticCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        $errs = $this->getTTDiffCntToCA($this->player_answer ?? "");
 | 
			
		||||
        $mark = $this->getMaxMark() + $errs * $this->getBadLineScore();
 | 
			
		||||
        $this->setMark($mark);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function toArray(string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = parent::toArray($mode);
 | 
			
		||||
 | 
			
		||||
        if ($mode === "all") {
 | 
			
		||||
            $a["bad_line_score"] = $this->bad_line_score;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $a["correct_answer"] = $this->getLogicFunction()->getTruthTable();
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,122 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "PicturedTask.php";
 | 
			
		||||
 | 
			
		||||
class VerilogTask extends PicturedTask
 | 
			
		||||
{
 | 
			
		||||
    private string $test_bench_fn; // test bench file name
 | 
			
		||||
    private string $compile_log; // short explanation for the marking
 | 
			
		||||
    private float $error_score; // points received for an error
 | 
			
		||||
    private const FAILED_MARK = "[FAILED]"; // mark at the beginning of testbench results indicating a failed combination
 | 
			
		||||
 | 
			
		||||
    public function __construct(array &$a = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct("verilog", $a);
 | 
			
		||||
 | 
			
		||||
        $this->compile_log = $a["compile_log"] ?? "";
 | 
			
		||||
        $this->test_bench_fn = $a["test_bench_fn"] ?? "";
 | 
			
		||||
        $this->error_score = $a["error_score"] ?? -1.0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function verifyCode(): bool
 | 
			
		||||
    {
 | 
			
		||||
        // check that no $function calls are in the code
 | 
			
		||||
        if (str_contains($this->player_answer, "$")) {
 | 
			
		||||
            $this->compile_log .= "A kód nem tartalmazhat \$függvényhívásokat!\n";
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function executeTest(): int
 | 
			
		||||
    {
 | 
			
		||||
        // store the user's answer
 | 
			
		||||
        $module_code_fn = tempnam(sys_get_temp_dir(), "verilogtask_user_module_");
 | 
			
		||||
        file_put_contents($module_code_fn, $this->getPlayerAnswer());
 | 
			
		||||
 | 
			
		||||
        // modify the test bench and save into a separate file
 | 
			
		||||
        $test_bench_fn = tempnam(sys_get_temp_dir(), "verilogtask_test_bench_");
 | 
			
		||||
        $include_line = "`include \"$module_code_fn\"\n\n";
 | 
			
		||||
        $tb = $include_line . file_get_contents($this->getGameDir() . DIRECTORY_SEPARATOR . $this->test_bench_fn);
 | 
			
		||||
        file_put_contents($test_bench_fn, $tb);
 | 
			
		||||
 | 
			
		||||
        // run the simulation
 | 
			
		||||
        $output_fn = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid("verilogtask_output_");
 | 
			
		||||
        $iverilog_cmd = "iverilog $test_bench_fn -o $output_fn -g2012 2>&1";
 | 
			
		||||
        $compilation_log = shell_exec($iverilog_cmd);
 | 
			
		||||
        $failed_count = 0;
 | 
			
		||||
        if (!is_null($compilation_log)) {
 | 
			
		||||
            $compilation_log = str_replace([$module_code_fn, $test_bench_fn], ["[kód]", "[tesztkörnyezet]"], $compilation_log);
 | 
			
		||||
            $this->compile_log .= "Fordítási hiba:\n\n" . (string)($compilation_log);
 | 
			
		||||
            $failed_count = PHP_INT_MAX;
 | 
			
		||||
            goto cleanup;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (file_exists($output_fn)) {
 | 
			
		||||
            $tb_output = shell_exec("$output_fn");
 | 
			
		||||
            $tb_output_lines = array_map(fn($line) => trim($line), explode("\n", $tb_output));
 | 
			
		||||
            $failed_trimlen = strlen(self::FAILED_MARK);
 | 
			
		||||
            foreach ($tb_output_lines as $line) {
 | 
			
		||||
                if (str_starts_with($line, self::FAILED_MARK)) {
 | 
			
		||||
                    $this->compile_log .= substr($line, $failed_trimlen) . "\n";
 | 
			
		||||
                    $failed_count++;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($failed_count == 0) {
 | 
			
		||||
                $this->compile_log .= "Minden rendben! :)";
 | 
			
		||||
            } else {
 | 
			
		||||
                $this->compile_log = "$failed_count db hiba:\n\n" . $this->compile_log;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        cleanup:
 | 
			
		||||
        // remove the temporary files
 | 
			
		||||
        @unlink($module_code_fn);
 | 
			
		||||
        @unlink($test_bench_fn);
 | 
			
		||||
        @unlink($output_fn);
 | 
			
		||||
 | 
			
		||||
        return $failed_count;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setErrorScore(float $error_score): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->error_score = $error_score;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getErrorScore(): float
 | 
			
		||||
    {
 | 
			
		||||
        return $this->error_score;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function staticCheck(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->compile_log = "";
 | 
			
		||||
 | 
			
		||||
        // verify code
 | 
			
		||||
        $mark = 0.0;
 | 
			
		||||
        if ($this->verifyCode()) {
 | 
			
		||||
            // run the simulation
 | 
			
		||||
            $failed_count = $this->executeTest();
 | 
			
		||||
            if ($failed_count != PHP_INT_MAX) {
 | 
			
		||||
                $mark = $this->getMaxMark() + $failed_count * $this->error_score;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->setMark($mark);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function toArray(string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = parent::toArray($mode);
 | 
			
		||||
 | 
			
		||||
        $a["compile_log"] = $this->compile_log;
 | 
			
		||||
 | 
			
		||||
        if ($mode == "all") {
 | 
			
		||||
            $a["test_bench_fn"] = $this->test_bench_fn;
 | 
			
		||||
            $a["error_score"] = $this->error_score;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										257
									
								
								class/Test.php
									
									
									
									
									
								
							
							
						
						
									
										257
									
								
								class/Test.php
									
									
									
									
									
								
							@ -1,257 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "TaskFactory.php";
 | 
			
		||||
 | 
			
		||||
require_once "TestSummary.php";
 | 
			
		||||
 | 
			
		||||
class Test extends AutoStoring
 | 
			
		||||
{
 | 
			
		||||
    const TEST_ONGOING = "ongoing";
 | 
			
		||||
    const TEST_CONCLUDED = "concluded";
 | 
			
		||||
 | 
			
		||||
    // ---------
 | 
			
		||||
 | 
			
		||||
    public int $id; // ID
 | 
			
		||||
    public int $gameId; // ID of associated game
 | 
			
		||||
    public string $gameName; // Name of the associated game
 | 
			
		||||
    public string $nickname; // Associated user's nickname
 | 
			
		||||
    public string $state; // State of the test (ongoing/concluded)
 | 
			
		||||
    public bool $timeLimited; // The user is allowed to submit the test in a given period of time.
 | 
			
		||||
    public bool $repeatable; // Is the user allowed to take this test multiple times?
 | 
			
		||||
    public int $startTime; // Start time (UNIX timestamp)
 | 
			
		||||
    public int $endTime; // End time (UNIX timestamp)
 | 
			
		||||
    public int $endLimitTime; // Time limit on test submission (UNIX timestamp)
 | 
			
		||||
    public TestSummary $summary; // Summmary, if game has ended
 | 
			
		||||
    public array $tasks; // Test tasks
 | 
			
		||||
 | 
			
		||||
    private TestMgr $testMgr; // Reference to TestMgr managing this Test instance
 | 
			
		||||
 | 
			
		||||
    // -------------
 | 
			
		||||
 | 
			
		||||
    // Preprocess tasks.
 | 
			
		||||
    private function preprocessTasks(): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->tasks as &$task) {
 | 
			
		||||
            $task->setGovernor($this); // set the task governor
 | 
			
		||||
            $task->setTemplate(false); // the task is no longer a template
 | 
			
		||||
            $task->randomize(); // randomize
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------
 | 
			
		||||
 | 
			
		||||
    function getMaxSumMark(): float
 | 
			
		||||
    {
 | 
			
		||||
        $msm = 0.0;
 | 
			
		||||
        foreach ($this->tasks as &$task) {
 | 
			
		||||
            $msm += $task->getMaxMark();
 | 
			
		||||
        }
 | 
			
		||||
        return $msm;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------
 | 
			
		||||
 | 
			
		||||
    // Store modifications.
 | 
			
		||||
    public function storeMods(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->testMgr->updateTest($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------
 | 
			
		||||
 | 
			
		||||
    // Construct new test based on Game and User objects
 | 
			
		||||
    function __construct(TestMgr &$testMgr, Game|array &$game_array, User &$user = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->testMgr = $testMgr;
 | 
			
		||||
        $this->id = -1;
 | 
			
		||||
 | 
			
		||||
        if (is_array($game_array)) { // populating fields from an array
 | 
			
		||||
            $a = &$game_array;
 | 
			
		||||
 | 
			
		||||
            $this->id = $a["_id"] ?? -1;
 | 
			
		||||
            $this->gameId = $a["gameid"];
 | 
			
		||||
            $this->gameName = $a["gamename"];
 | 
			
		||||
            $this->nickname = $a["nickname"];
 | 
			
		||||
            $this->state = $a["state"];
 | 
			
		||||
            $this->timeLimited = $a["time_limited"];
 | 
			
		||||
            $this->startTime = $a["start_time"];
 | 
			
		||||
            $this->endTime = $a["end_time"] ?? 0;
 | 
			
		||||
            $this->endLimitTime = $a["end_limit_time"] ?? 0;
 | 
			
		||||
            $this->repeatable = $a["repeatable"];
 | 
			
		||||
            $this->tasks = TaskFactory::constructFromCollection($a["challenges"], $this);
 | 
			
		||||
            if (isset($a["summary"])) {
 | 
			
		||||
                $this->summary = TestSummary::fromArray($a["summary"]);
 | 
			
		||||
            } else { // backward compatibility
 | 
			
		||||
                $this->summary = new TestSummary($this->getMaxSumMark(), 0);
 | 
			
		||||
            }
 | 
			
		||||
        } else { // populating fields from Game and User objects
 | 
			
		||||
            $game = &$game_array;
 | 
			
		||||
 | 
			
		||||
            $this->endTime = 0;
 | 
			
		||||
 | 
			
		||||
            // Fill-in basic properties
 | 
			
		||||
            $this->gameId = $game->getId();
 | 
			
		||||
            $this->gameName = $game->getName();
 | 
			
		||||
            $this->tasks = $game->getTasks();
 | 
			
		||||
            $this->preprocessTasks();
 | 
			
		||||
            $this->nickname = $user->getNickname();
 | 
			
		||||
 | 
			
		||||
            $this->state = self::TEST_ONGOING;
 | 
			
		||||
            $gp = $game->getProperties();
 | 
			
		||||
            $this->timeLimited = (($gp["time_limit"] ?: -1) > -1);
 | 
			
		||||
 | 
			
		||||
            $now = time();
 | 
			
		||||
            $this->startTime = $now;
 | 
			
		||||
            if ($this->timeLimited) {
 | 
			
		||||
                $this->endLimitTime = $now + $gp["time_limit"];
 | 
			
		||||
            } else {
 | 
			
		||||
                $this->endLimitTime = -1; // dummy value, not used, since timeLimited is false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->repeatable = $gp["repeatable"];
 | 
			
		||||
 | 
			
		||||
            // Create a blank summary
 | 
			
		||||
            $this->summary = new TestSummary($this->getMaxSumMark(), 0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // auto-conclude time-constrained test if expired
 | 
			
		||||
        if ($this->timeLimited && $this->isOngoing() && ($this->endLimitTime <= time())) {
 | 
			
		||||
            $this->concludeTest();
 | 
			
		||||
            $this->endTime = $this->endLimitTime; // date back end time to the limiting value
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convert test to array.
 | 
			
		||||
    function toArray(array $omit = [], string $mode = "all"): array
 | 
			
		||||
    {
 | 
			
		||||
        $tasks = [];
 | 
			
		||||
        foreach ($this->tasks as &$t) {
 | 
			
		||||
            $tasks[] = $t->toArray($mode);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $a = [
 | 
			
		||||
            "_id" => $this->id,
 | 
			
		||||
            "gameid" => $this->gameId,
 | 
			
		||||
            "nickname" => $this->nickname,
 | 
			
		||||
            "gamename" => $this->gameName,
 | 
			
		||||
            "state" => $this->state,
 | 
			
		||||
            "time_limited" => $this->timeLimited,
 | 
			
		||||
            "start_time" => $this->startTime,
 | 
			
		||||
            "end_time" => $this->endTime,
 | 
			
		||||
            "end_limit_time" => $this->endLimitTime,
 | 
			
		||||
            "repeatable" => $this->repeatable,
 | 
			
		||||
            "challenges" => $tasks,
 | 
			
		||||
            "summary" => $this->summary->toArray()
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // omit specific fields
 | 
			
		||||
        foreach ($omit as $field) {
 | 
			
		||||
            unset($a[$field]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get number of tasks.
 | 
			
		||||
    function getTaskCount(): int
 | 
			
		||||
    {
 | 
			
		||||
        return count($this->tasks);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function isTaskIdInsideBounds(int $tidx): bool {
 | 
			
		||||
        return ($tidx >= 0) && ($tidx < $this->getTaskCount());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Save answer. Asserting $safe prevents saving answers to a concluded test.
 | 
			
		||||
    function saveAnswer(int $tidx, int|string $ans, bool $safe = true): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (!$safe || $this->state === self::TEST_ONGOING) {
 | 
			
		||||
            if ($this->isTaskIdInsideBounds($tidx)) {
 | 
			
		||||
                $this->tasks[$tidx]->saveAnswer($ans);
 | 
			
		||||
                $this->commitMods();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Clear answer.
 | 
			
		||||
    function clearAnswer(int $chidx, bool $safe = true): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (!$safe || $this->state === self::TEST_ONGOING) {
 | 
			
		||||
            if ($this->isTaskIdInsideBounds($chidx)) {
 | 
			
		||||
                $this->tasks[$chidx]->clearAnswer();
 | 
			
		||||
                $this->commitMods();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Conclude test.
 | 
			
		||||
    function concludeTest(): void
 | 
			
		||||
    {
 | 
			
		||||
        // summarize points
 | 
			
		||||
        $mark_sum = 0.0;
 | 
			
		||||
        foreach ($this->tasks as &$ch) {
 | 
			
		||||
            $ch->autoCheck();
 | 
			
		||||
            $mark_sum += $ch->getMark();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // set state and fill summary
 | 
			
		||||
        $this->state = self::TEST_CONCLUDED;
 | 
			
		||||
        $this->endTime = time();
 | 
			
		||||
        $this->summary->setMark($mark_sum);
 | 
			
		||||
 | 
			
		||||
        // save test
 | 
			
		||||
        $this->commitMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // --------
 | 
			
		||||
 | 
			
		||||
    public function getId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStartTime(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->startTime;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getEndTime(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->endTime;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getSummary(): TestSummary
 | 
			
		||||
    {
 | 
			
		||||
        return $this->summary;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getNickname(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->nickname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getGameId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->gameId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isConcluded(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->state === self::TEST_CONCLUDED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isOngoing(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->state === self::TEST_ONGOING;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getGameDir(): string {
 | 
			
		||||
        return Game::getGameDirById($this->gameId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -8,7 +8,297 @@ require_once "ExpressionBuilder.php";
 | 
			
		||||
 | 
			
		||||
require_once "globals.php";
 | 
			
		||||
 | 
			
		||||
require_once "Test.php";
 | 
			
		||||
const TEST_ONGOING = "ongoing";
 | 
			
		||||
const TEST_CONCLUDED = "concluded";
 | 
			
		||||
 | 
			
		||||
class TestSummary
 | 
			
		||||
{
 | 
			
		||||
    public int $challengeN; // Number of challenges
 | 
			
		||||
    public int $correctAnswerN; // 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;
 | 
			
		||||
        } else { // avoid division by zero
 | 
			
		||||
            $this->percentage = 0.0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function __construct(int $challengeN, int $correctAnswerN)
 | 
			
		||||
    {
 | 
			
		||||
        $this->challengeN = $challengeN;
 | 
			
		||||
        $this->correctAnswerN = $correctAnswerN;
 | 
			
		||||
        $this->calculatePercentage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get challenge count.
 | 
			
		||||
    function getChallengeN(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->challengeN;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get number of correct answers.
 | 
			
		||||
    function getCorrectAnswerN(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->correctAnswerN;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setCorrectAnswerN(int $correctAnswerN): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->correctAnswerN = $correctAnswerN;
 | 
			
		||||
        $this->calculatePercentage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get ratio of correct results.
 | 
			
		||||
    function getPercentage(): float
 | 
			
		||||
    {
 | 
			
		||||
        return ($this->correctAnswerN * 100.0) / $this->challengeN;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Build from array.
 | 
			
		||||
    static function fromArray(array $a): TestSummary
 | 
			
		||||
    {
 | 
			
		||||
        return new TestSummary($a["challenge_n"], $a["correct_answer_n"]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convert to array.
 | 
			
		||||
    function toArray(): array
 | 
			
		||||
    {
 | 
			
		||||
        return ["challenge_n" => $this->challengeN, "correct_answer_n" => $this->correctAnswerN, "percentage" => $this->percentage];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Test extends AutoStoring
 | 
			
		||||
{
 | 
			
		||||
    const TEST_ONGOING = "ongoing";
 | 
			
		||||
    const TEST_CONCLUDED = "concluded";
 | 
			
		||||
 | 
			
		||||
    // ---------
 | 
			
		||||
 | 
			
		||||
    public int $id; // ID
 | 
			
		||||
    public int $gameId; // ID of associated game
 | 
			
		||||
    public string $gameName; // Name of the associated game
 | 
			
		||||
    public string $nickname; // Associated user's nickname
 | 
			
		||||
    public string $state; // State of the test (ongoing/concluded)
 | 
			
		||||
    public bool $timeLimited; // The user is allowed to submit the test in a given period of time.
 | 
			
		||||
    public bool $repeatable; // Is the user allowed to take this test multiple times?
 | 
			
		||||
    public int $startTime; // Start time (UNIX timestamp)
 | 
			
		||||
    public int $endTime; // End time (UNIX timestamp)
 | 
			
		||||
    public int $endLimitTime; // Time limit on test submission (UNIX timestamp)
 | 
			
		||||
    public TestSummary $summary; // Summmary, if game has ended
 | 
			
		||||
    public array $challenges; // Test challenges
 | 
			
		||||
 | 
			
		||||
    private TestMgr $testMgr; // Reference to TestMgr managing this Test instance
 | 
			
		||||
 | 
			
		||||
    // -------------
 | 
			
		||||
 | 
			
		||||
    // Preprocess challenges.
 | 
			
		||||
    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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------
 | 
			
		||||
 | 
			
		||||
    // Store modifications.
 | 
			
		||||
    public function storeMods(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->testMgr->updateTest($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------
 | 
			
		||||
 | 
			
		||||
    // Construct new test based on Game and User objects
 | 
			
		||||
    function __construct(TestMgr &$testMgr, Game|array &$game_array, User &$user = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->testMgr = $testMgr;
 | 
			
		||||
        $this->id = -1;
 | 
			
		||||
 | 
			
		||||
        if (is_array($game_array)) { // populating fields from an array
 | 
			
		||||
            $a = &$game_array;
 | 
			
		||||
 | 
			
		||||
            $this->id = $a["_id"] ?? -1;
 | 
			
		||||
            $this->gameId = $a["gameid"];
 | 
			
		||||
            $this->gameName = $a["gamename"];
 | 
			
		||||
            $this->nickname = $a["nickname"];
 | 
			
		||||
            $this->state = $a["state"];
 | 
			
		||||
            $this->timeLimited = $a["time_limited"];
 | 
			
		||||
            $this->startTime = $a["start_time"];
 | 
			
		||||
            $this->endTime = $a["end_time"] ?? 0;
 | 
			
		||||
            $this->endLimitTime = $a["end_limit_time"] ?? 0;
 | 
			
		||||
            $this->repeatable = $a["repeatable"];
 | 
			
		||||
            $this->challenges = $a["challenges"];
 | 
			
		||||
            if (isset($a["summary"])) {
 | 
			
		||||
                $this->summary = TestSummary::fromArray($a["summary"]);
 | 
			
		||||
            } else { // backward compatibility
 | 
			
		||||
                $this->summary = new TestSummary(count($a["challenges"]), 0);
 | 
			
		||||
            }
 | 
			
		||||
        } else { // populating fields from Game and User objects
 | 
			
		||||
            $game = &$game_array;
 | 
			
		||||
 | 
			
		||||
            $this->endTime = 0;
 | 
			
		||||
 | 
			
		||||
            // Fill-in basic properties
 | 
			
		||||
            $this->gameId = $game->getId();
 | 
			
		||||
            $this->gameName = $game->getName();
 | 
			
		||||
            $this->challenges = $game->getChallenges();
 | 
			
		||||
            $this->preprocessChallenges();
 | 
			
		||||
            $this->nickname = $user->getNickname();
 | 
			
		||||
 | 
			
		||||
            $this->state = self::TEST_ONGOING;
 | 
			
		||||
            $gp = $game->getProperties();
 | 
			
		||||
            $this->timeLimited = (($gp["time_limit"] ?: -1) > -1);
 | 
			
		||||
 | 
			
		||||
            $now = time();
 | 
			
		||||
            $this->startTime = $now;
 | 
			
		||||
            if ($this->timeLimited) {
 | 
			
		||||
                $this->endLimitTime = $now + $gp["time_limit"];
 | 
			
		||||
            } else {
 | 
			
		||||
                $this->endLimitTime = -1; // dummy value, not used, since timeLimited is false
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $this->repeatable = $gp["repeatable"];
 | 
			
		||||
 | 
			
		||||
            // Create a blank summary
 | 
			
		||||
            $this->summary = new TestSummary(count($this->challenges), 0);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // auto-conclude time-constrained test if expired
 | 
			
		||||
        if ($this->timeLimited && $this->isOngoing() && ($this->endLimitTime <= time())) {
 | 
			
		||||
            $this->concludeTest();
 | 
			
		||||
            $this->endTime = $this->endLimitTime; // date back end time to the limiting value
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convert test to array.
 | 
			
		||||
    function toArray(array $omit = []): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = [
 | 
			
		||||
            "_id" => $this->id,
 | 
			
		||||
            "gameid" => $this->gameId,
 | 
			
		||||
            "nickname" => $this->nickname,
 | 
			
		||||
            "gamename" => $this->gameName,
 | 
			
		||||
            "state" => $this->state,
 | 
			
		||||
            "time_limited" => $this->timeLimited,
 | 
			
		||||
            "start_time" => $this->startTime,
 | 
			
		||||
            "end_time" => $this->endTime,
 | 
			
		||||
            "end_limit_time" => $this->endLimitTime,
 | 
			
		||||
            "repeatable" => $this->repeatable,
 | 
			
		||||
            "challenges" => $this->challenges,
 | 
			
		||||
            "summary" => $this->summary->toArray()
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // omit specific fields
 | 
			
		||||
        foreach ($omit as $field) {
 | 
			
		||||
            unset($a[$field]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get number of challenges.
 | 
			
		||||
    function getChallengeCount(): int
 | 
			
		||||
    {
 | 
			
		||||
        return count($this->challenges);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Save answer. Asserting $safe prevents saving answers to a concluded test.
 | 
			
		||||
    function saveAnswer(int $chidx, int $ansidx, 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;
 | 
			
		||||
                $this->commitMods();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Clear answer.
 | 
			
		||||
    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;
 | 
			
		||||
                $this->commitMods();
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Conclude test.
 | 
			
		||||
    function concludeTest(): void
 | 
			
		||||
    {
 | 
			
		||||
        // check the answers
 | 
			
		||||
        $cans_n = 0; // number of correct answers
 | 
			
		||||
        foreach ($this->challenges as &$ch) {
 | 
			
		||||
            if ($ch["player_answer"] === $ch["correct_answer"]) {
 | 
			
		||||
                $cans_n++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // set state and fill summary
 | 
			
		||||
        $this->state = TEST_CONCLUDED;
 | 
			
		||||
        $this->endTime = time();
 | 
			
		||||
        $this->summary->setCorrectAnswerN($cans_n);
 | 
			
		||||
 | 
			
		||||
        // save test
 | 
			
		||||
        $this->commitMods();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // --------
 | 
			
		||||
 | 
			
		||||
    public function getId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStartTime(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->startTime;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getEndTime(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->endTime;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getSummary(): TestSummary
 | 
			
		||||
    {
 | 
			
		||||
        return $this->summary;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getNickname(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->nickname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getGameId(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->gameId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isConcluded(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->state === self::TEST_CONCLUDED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isOngoing(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->state === self::TEST_ONGOING;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class TestMgr
 | 
			
		||||
{
 | 
			
		||||
@ -71,7 +361,7 @@ class TestMgr
 | 
			
		||||
        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]]);
 | 
			
		||||
            $ongoing_tests = $this->db->findBy([$fetch_criteria, "AND", ["state", "=", TEST_ONGOING]]);
 | 
			
		||||
            if (count($ongoing_tests) !== 0) { // if there's an ongoing test
 | 
			
		||||
                $testid = $ongoing_tests[0]["_id"];
 | 
			
		||||
                $test = $this->getTest($testid);
 | 
			
		||||
@ -116,7 +406,7 @@ class TestMgr
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
    function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_challenge_data, bool $best_ones_only, array ...$furtherFilters): array
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->db->createQueryBuilder();
 | 
			
		||||
        $qb = $qb->where(["gameid", "=", (int)$gameid]);
 | 
			
		||||
@ -152,8 +442,8 @@ class TestMgr
 | 
			
		||||
            $qb->orderBy($ordering);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // excluding task data
 | 
			
		||||
        if ($exclude_task_data) {
 | 
			
		||||
        // excluding challenge data
 | 
			
		||||
        if ($exclude_challenge_data) {
 | 
			
		||||
            $qb->except(["challenges"]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -204,42 +494,42 @@ class TestMgr
 | 
			
		||||
        $qb->select(["challenges"]);
 | 
			
		||||
        $entries = $qb->getQuery()->fetch();
 | 
			
		||||
 | 
			
		||||
        $task_indices = [];
 | 
			
		||||
        $challenge_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"];
 | 
			
		||||
            foreach ($entry["challenges"] as $challenge) {
 | 
			
		||||
                $correct_answer = $challenge["answers"][$challenge["correct_answer"]];
 | 
			
		||||
                $compound = $challenge["question"] . $correct_answer . count($challenge["answers"]) . $challenge["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
 | 
			
		||||
                // if this is a new challenge to the list...
 | 
			
		||||
                if (!isset($challenge_indices[$idhash])) {
 | 
			
		||||
                    $challenge_indices[$idhash] = count($challenge_indices);
 | 
			
		||||
                    $challenge_info = [ // copy challenge info
 | 
			
		||||
                        "hash" => $idhash,
 | 
			
		||||
                        "image_url" => $task["image_url"],
 | 
			
		||||
                        "question" => $task["question"],
 | 
			
		||||
                        "answers" => $task["answers"],
 | 
			
		||||
                        "image_url" => $challenge["image_url"],
 | 
			
		||||
                        "question" => $challenge["question"],
 | 
			
		||||
                        "answers" => $challenge["answers"],
 | 
			
		||||
                        "correct_answer" => $correct_answer,
 | 
			
		||||
                        "player_answers" => array_fill(0, count($task["answers"]), 0),
 | 
			
		||||
                        "answer_count" => count($task["answers"]),
 | 
			
		||||
                        "player_answers" => array_fill(0, count($challenge["answers"]), 0),
 | 
			
		||||
                        "answer_count" => count($challenge["answers"]),
 | 
			
		||||
                        "skipped" => 0
 | 
			
		||||
                    ];
 | 
			
		||||
                    $aggregated[$task_indices[$idhash]] = $task_info; // insert task info
 | 
			
		||||
                    $aggregated[$challenge_indices[$idhash]] = $challenge_info; // insert challenge info
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // fetch task index
 | 
			
		||||
                $task_idx = $task_indices[$idhash];
 | 
			
		||||
                // fetch challenge index
 | 
			
		||||
                $challenge_idx = $challenge_indices[$idhash];
 | 
			
		||||
 | 
			
		||||
                // add up player answer
 | 
			
		||||
                $player_answer = trim($task["player_answer"]);
 | 
			
		||||
                $player_answer = trim($challenge["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]++;
 | 
			
		||||
                    $answer_idx = array_search($challenge["answers"][$challenge["player_answer"]], $aggregated[$challenge_idx]["answers"]); // transform player answer index to report answer index
 | 
			
		||||
                    $aggregated[$challenge_idx]["player_answers"][(int)$answer_idx]++;
 | 
			
		||||
                } else { // player has not answered or provided an unprocessable answer
 | 
			
		||||
                    $aggregated[$task_idx]["skipped"]++;
 | 
			
		||||
                    $aggregated[$challenge_idx]["skipped"]++;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -259,7 +549,7 @@ class TestMgr
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // match tasks
 | 
			
		||||
        // match challenges
 | 
			
		||||
        return $aggregated;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -284,7 +574,7 @@ class TestMgr
 | 
			
		||||
    {
 | 
			
		||||
        $query = [["time_limited", "=", true], "AND", ["end_limit_time", "<", time()]];
 | 
			
		||||
        if ($ongoingOnly) {
 | 
			
		||||
            $query = [...$query, "AND", ["state", "=", Test::TEST_ONGOING]];
 | 
			
		||||
            $query = [...$query, "AND", ["state", "=", TEST_ONGOING]];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $qb = $this->db->createQueryBuilder();
 | 
			
		||||
 | 
			
		||||
@ -1,65 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
class TestSummary
 | 
			
		||||
{
 | 
			
		||||
    public int $maxMark; // Maximum mark
 | 
			
		||||
    public int $mark; // Collected mark
 | 
			
		||||
    private float $percentage; // Ratio of correct answers
 | 
			
		||||
 | 
			
		||||
    // Calculate percentage.
 | 
			
		||||
    private function calculatePercentage(): void
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->maxMark > 0) {
 | 
			
		||||
            $this->percentage = $this->mark / (double)$this->maxMark * 100.0;
 | 
			
		||||
        } else { // avoid division by zero
 | 
			
		||||
            $this->percentage = 0.0;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function __construct(int $taskN, int $correctAnswerN)
 | 
			
		||||
    {
 | 
			
		||||
        $this->maxMark = $taskN;
 | 
			
		||||
        $this->mark = $correctAnswerN;
 | 
			
		||||
        $this->calculatePercentage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get max mark.
 | 
			
		||||
    function getMaxMark(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->maxMark;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get mark.
 | 
			
		||||
    function getMark(): int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->mark;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setMark(int $mark): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->mark = $mark;
 | 
			
		||||
        $this->calculatePercentage();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get ratio of correct results.
 | 
			
		||||
    function getPercentage(): float
 | 
			
		||||
    {
 | 
			
		||||
        return ($this->mark * 100.0) / $this->maxMark;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Build from array.
 | 
			
		||||
    static function fromArray(array $a): TestSummary
 | 
			
		||||
    {
 | 
			
		||||
        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->maxMark, "correct_answer_n" => $this->mark, "percentage" => $this->percentage];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										134
									
								
								class/User.php
									
									
									
									
									
								
							
							
						
						
									
										134
									
								
								class/User.php
									
									
									
									
									
								
							@ -1,134 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "AutoStoring.php";
 | 
			
		||||
 | 
			
		||||
class User extends AutoStoring
 | 
			
		||||
{
 | 
			
		||||
    private int $id; // User's ID
 | 
			
		||||
    private string $nickname; // User's nickname
 | 
			
		||||
    private string $password; // User's password in it's encoded form or left empty
 | 
			
		||||
    private string $realname; // User's real name displayed in their profile
 | 
			
		||||
//    private array $groups; // User's assigned groups
 | 
			
		||||
    private string $privilege; // User's privilege
 | 
			
		||||
    private UserMgr $userMgr; // UserManager object governing this object.
 | 
			
		||||
 | 
			
		||||
    // -------------------------------------------
 | 
			
		||||
 | 
			
		||||
    // Store modifications to the database.
 | 
			
		||||
    public function storeMods(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->userMgr->updateUser($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------------------------------------
 | 
			
		||||
 | 
			
		||||
    function __construct(UserMgr &$usrmgr, int $id, string $nickname = null, string $password = null, string $realname = null, string $privilege = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->id = $id;
 | 
			
		||||
        $this->nickname = $nickname;
 | 
			
		||||
        $this->password = $password;
 | 
			
		||||
        $this->realname = $realname;
 | 
			
		||||
//        $this->groups = $groups;
 | 
			
		||||
        $this->privilege = $privilege;
 | 
			
		||||
 | 
			
		||||
        // save reference to user manager
 | 
			
		||||
        $this->userMgr = &$usrmgr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create user from an array
 | 
			
		||||
    static function fromArray(UserMgr &$usrmgr, array $a): User
 | 
			
		||||
    {
 | 
			
		||||
        $id = $a["_id"] ?? -1;
 | 
			
		||||
        return new User($usrmgr, $id, $a["nickname"], $a["password"], $a["realname"], $a["privilege"]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convert user to array
 | 
			
		||||
    function toArray(array $omit = []): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = [
 | 
			
		||||
            "_id" => $this->id,
 | 
			
		||||
            "nickname" => $this->nickname,
 | 
			
		||||
            "password" => $this->password,
 | 
			
		||||
            "realname" => $this->realname,
 | 
			
		||||
//            "groups" => $this->groups,
 | 
			
		||||
            "privilege" => $this->privilege
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // omit specific fields
 | 
			
		||||
        foreach ($omit as $field) {
 | 
			
		||||
            unset($a[$field]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Change user password. If $safe, then $old is checked.
 | 
			
		||||
    function changePassword(string $new, string $old, bool $safe = true): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (!$safe || password_verify($old, $this->password)) {
 | 
			
		||||
            $this->password = password_hash($new, PASSWORD_DEFAULT);
 | 
			
		||||
            $this->storeMods(); // store modifications
 | 
			
		||||
            return true;
 | 
			
		||||
        } else {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
//    // Change user groups
 | 
			
		||||
//    function changeGroups(array $add, array $remove): void
 | 
			
		||||
//    {
 | 
			
		||||
//        alter_array_contents($this->groups, $add, $remove);
 | 
			
		||||
//        $this->storeMods(); // store modifications
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
//    // Get user's groups
 | 
			
		||||
//    function getGroups(): array
 | 
			
		||||
//    {
 | 
			
		||||
//        return $this->groups;
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    // Set user privilege level
 | 
			
		||||
    function setPrivilege(string $privilege): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->privilege = ($this->nickname === QUIZMASTER_NICKNAME) ? PRIVILEGE_QUIZMASTER : $privilege; // quizmaster's privilege mustn't be tampered with
 | 
			
		||||
        $this->storeMods(); // store modifications
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get user privilege level
 | 
			
		||||
    function getPrivilege(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->privilege;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get user's nickname.
 | 
			
		||||
    function getNickname(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->nickname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set user's real name.
 | 
			
		||||
    function setRealname(string $realname): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->realname = $realname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get user's real name.
 | 
			
		||||
    function getRealname(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->realname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check against user credentials.
 | 
			
		||||
    function checkPassword(string $password): bool
 | 
			
		||||
    {
 | 
			
		||||
        return password_verify($password, $this->password);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Has the user quizmaster privileges?
 | 
			
		||||
    function hasQuizmasterPrivilege(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->privilege == PRIVILEGE_QUIZMASTER;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -8,7 +8,136 @@ require_once "AutoStoring.php";
 | 
			
		||||
 | 
			
		||||
require_once "privilege_levels.php";
 | 
			
		||||
 | 
			
		||||
require_once "User.php";
 | 
			
		||||
class User extends AutoStoring
 | 
			
		||||
{
 | 
			
		||||
    private int $id; // User's ID
 | 
			
		||||
    private string $nickname; // User's nickname
 | 
			
		||||
    private string $password; // User's password in it's encoded form or left empty
 | 
			
		||||
    private string $realname; // User's real name displayed in their profile
 | 
			
		||||
//    private array $groups; // User's assigned groups
 | 
			
		||||
    private string $privilege; // User's privilege
 | 
			
		||||
    private UserMgr $userMgr; // UserManager object governing this object.
 | 
			
		||||
 | 
			
		||||
    // -------------------------------------------
 | 
			
		||||
 | 
			
		||||
    // Store modifications to the database.
 | 
			
		||||
    public function storeMods(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->userMgr->updateUser($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // -------------------------------------------
 | 
			
		||||
 | 
			
		||||
    function __construct(UserMgr &$usrmgr, int $id, string $nickname = null, string $password = null, string $realname = null, string $privilege = null)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct();
 | 
			
		||||
 | 
			
		||||
        $this->id = $id;
 | 
			
		||||
        $this->nickname = $nickname;
 | 
			
		||||
        $this->password = $password;
 | 
			
		||||
        $this->realname = $realname;
 | 
			
		||||
//        $this->groups = $groups;
 | 
			
		||||
        $this->privilege = $privilege;
 | 
			
		||||
 | 
			
		||||
        // save reference to user manager
 | 
			
		||||
        $this->userMgr = &$usrmgr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Create user from an array
 | 
			
		||||
    static function fromArray(UserMgr &$usrmgr, array $a): User
 | 
			
		||||
    {
 | 
			
		||||
        $id = $a["_id"] ?? -1;
 | 
			
		||||
        return new User($usrmgr, $id, $a["nickname"], $a["password"], $a["realname"], $a["privilege"]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Convert user to array
 | 
			
		||||
    function toArray(array $omit = []): array
 | 
			
		||||
    {
 | 
			
		||||
        $a = [
 | 
			
		||||
            "_id" => $this->id,
 | 
			
		||||
            "nickname" => $this->nickname,
 | 
			
		||||
            "password" => $this->password,
 | 
			
		||||
            "realname" => $this->realname,
 | 
			
		||||
//            "groups" => $this->groups,
 | 
			
		||||
            "privilege" => $this->privilege
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // omit specific fields
 | 
			
		||||
        foreach ($omit as $field) {
 | 
			
		||||
            unset($a[$field]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Change user password. If $safe, then $old is checked.
 | 
			
		||||
    function changePassword(string $new, string $old, bool $safe = true): bool
 | 
			
		||||
    {
 | 
			
		||||
        if (!$safe || password_verify($old, $this->password)) {
 | 
			
		||||
            $this->password = password_hash($new, PASSWORD_DEFAULT);
 | 
			
		||||
            $this->storeMods(); // store modifications
 | 
			
		||||
            return true;
 | 
			
		||||
        } else {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
//    // Change user groups
 | 
			
		||||
//    function changeGroups(array $add, array $remove): void
 | 
			
		||||
//    {
 | 
			
		||||
//        alter_array_contents($this->groups, $add, $remove);
 | 
			
		||||
//        $this->storeMods(); // store modifications
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
//    // Get user's groups
 | 
			
		||||
//    function getGroups(): array
 | 
			
		||||
//    {
 | 
			
		||||
//        return $this->groups;
 | 
			
		||||
//    }
 | 
			
		||||
 | 
			
		||||
    // Set user privilege level
 | 
			
		||||
    function setPrivilege(string $privilege): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->privilege = ($this->nickname === QUIZMASTER_NICKNAME) ? PRIVILEGE_QUIZMASTER : $privilege; // quizmaster's privilege mustn't be tampered with
 | 
			
		||||
        $this->storeMods(); // store modifications
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get user privilege level
 | 
			
		||||
    function getPrivilege(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->privilege;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get user's nickname.
 | 
			
		||||
    function getNickname(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->nickname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Set user's real name.
 | 
			
		||||
    function setRealname(string $realname): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->realname = $realname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get user's real name.
 | 
			
		||||
    function getRealname(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->realname;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check against user credentials.
 | 
			
		||||
    function checkPassword(string $password): bool
 | 
			
		||||
    {
 | 
			
		||||
        return password_verify($password, $this->password);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Has the user quizmaster privileges?
 | 
			
		||||
    function hasQuizmasterPrivilege(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->privilege == PRIVILEGE_QUIZMASTER;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class UserMgr
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
class Utils
 | 
			
		||||
{
 | 
			
		||||
    public const WORKSPACE_DIR = "workspace";
 | 
			
		||||
 | 
			
		||||
    public static function str2kv(string $str): array
 | 
			
		||||
    {
 | 
			
		||||
        preg_match_all("/([^,= ]+)=([^,= ]+)/", $str, $r);
 | 
			
		||||
        $a = array_combine($r[1], $r[2]);
 | 
			
		||||
 | 
			
		||||
        foreach ($a as &$v) {
 | 
			
		||||
            if (is_numeric($v)) { // is it a numeric value?
 | 
			
		||||
                if (((int)$v) == ((double)$v)) { // is it an integer?
 | 
			
		||||
                    $v = (int)$v;
 | 
			
		||||
                } else { // is it a float?
 | 
			
		||||
                    $v = (double)$v;
 | 
			
		||||
                }
 | 
			
		||||
            } else if (in_array(strtolower($v), ["true", "false"]) ) { // it's a boolean
 | 
			
		||||
                $v = $v === "true";
 | 
			
		||||
            } else if (str_starts_with($v, '"') && str_ends_with($v, '"')) { // it's a string
 | 
			
		||||
                $v = substr($v, 1, strlen($v) - 2); // strip leading and trailing quotes
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return $a;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function str2a(string $str): array {
 | 
			
		||||
        return explode(",", $str);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function getWorkspaceDir(): string {
 | 
			
		||||
        return getcwd() . DIRECTORY_SEPARATOR . self::WORKSPACE_DIR;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
class VerilogUtils
 | 
			
		||||
{
 | 
			
		||||
    public static function genTruthTable(): string {
 | 
			
		||||
        // collect input variables
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,17 +1,10 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
require_once "vendor/autoload.php";
 | 
			
		||||
 | 
			
		||||
require_once "class/TestMgr.php";
 | 
			
		||||
require_once "class/GameMgr.php";
 | 
			
		||||
 | 
			
		||||
require_once "class/LogicFunction.php";
 | 
			
		||||
 | 
			
		||||
ini_set('display_errors', 1);
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
@ -29,37 +22,12 @@ if (isset($options["action"])) {
 | 
			
		||||
                printf("OK!\n");
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case "upgrade_games":
 | 
			
		||||
            {
 | 
			
		||||
                printf("Upgrading games...");
 | 
			
		||||
                $gameMgr = new GameMgr();
 | 
			
		||||
                $gameMgr->upgradeGames();
 | 
			
		||||
                printf("OK!\n");
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case "get_timed_tests":
 | 
			
		||||
            {
 | 
			
		||||
                $testMgr = new TestMgr();
 | 
			
		||||
                printf("Expired timed tests: %s\n", join(", ", $testMgr->extractExpiredTimedTestIds()));
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case "gen_random":
 | 
			
		||||
            {
 | 
			
		||||
                $lf = LogicFunction::genRandom(["a", "b", "c"], 2, 4);
 | 
			
		||||
                //$lf = LogicFunction::genRandomDF(["a", "b", "c"]);
 | 
			
		||||
                printf("Verilog-form: %s\nTeX-form: %s\n", $lf->getExpression(), $lf->getExpression("tex"));
 | 
			
		||||
                print_r($lf->getTruthTable());
 | 
			
		||||
                print_r($lf->toDNF());
 | 
			
		||||
                //$lf->drawNetwork("TESTING/network.svg");
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case "verify":
 | 
			
		||||
            {
 | 
			
		||||
                printf("Verifying expression\n");
 | 
			
		||||
                $ok = LogicFunction::isCorrectDNF(["a", "b", "c"], "(a & ~b & c) | (b & ~c & a)");
 | 
			
		||||
                printf("%d\n", $ok);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
 | 
			
		||||
function explode_list(string $str) : array {
 | 
			
		||||
    if (($str = trim($str, " \n\r\t\v\0,")) !== "") {
 | 
			
		||||
        return array_map(fn($a) => trim($a), explode(",", trim($str)));
 | 
			
		||||
        return array_map(fn($a) => trim($a), explode(",", str_replace(" ", "", $str)));
 | 
			
		||||
    } else {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -4,9 +4,6 @@
 | 
			
		||||
    "ext-http": "*",
 | 
			
		||||
    "ext-mbstring" : "*",
 | 
			
		||||
    "ext-zip": "*",
 | 
			
		||||
    "ext-fileinfo": "*",
 | 
			
		||||
    "ext-luasandbox": "*",
 | 
			
		||||
    "phpoffice/phpspreadsheet": "^5.1",
 | 
			
		||||
    "symfony/expression-language": "^7.3"
 | 
			
		||||
    "ext-fileinfo": "*"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -71,10 +71,10 @@ if (!get_autologin_state() || (($user_data["privilege"] !== PRIVILEGE_CREATOR) &
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td><label>Kérdés-fájlok:</label></td>
 | 
			
		||||
                        <td>
 | 
			
		||||
                            <input type="button" id="download_tasks_btn" value="Letöltés CSV-ként" shown="false"
 | 
			
		||||
                                   onclick="download_tasks()">
 | 
			
		||||
                            <input type="button" id="edit_tasks_btn" value="Szerkesztés"
 | 
			
		||||
                                   onclick="edit_tasks()" shown="false">
 | 
			
		||||
                            <input type="button" id="download_challenges_btn" value="Letöltés CSV-ként" shown="false"
 | 
			
		||||
                                   onclick="download_challenges()">
 | 
			
		||||
                            <input type="button" id="edit_challenges_btn" value="Szerkesztés"
 | 
			
		||||
                                   onclick="edit_challenges()" shown="false">
 | 
			
		||||
                            <input type="button" value="Új feltöltése" id="show_game_file_upload"
 | 
			
		||||
                                   onclick="show_hide_gamefile_upload(true)">
 | 
			
		||||
                            <input type="file" id="game_file" shown="false">
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										124
									
								
								interface.php
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								interface.php
									
									
									
									
									
								
							@ -1,7 +1,5 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
ini_set("display_errors", true);
 | 
			
		||||
 | 
			
		||||
require_once "check_maintenance.php";
 | 
			
		||||
 | 
			
		||||
//ini_set('display_startup_errors', '1');
 | 
			
		||||
@ -32,10 +30,6 @@ require_once "class/TestMgr.php";
 | 
			
		||||
 | 
			
		||||
require_once "class/ReportBuilder.php";
 | 
			
		||||
 | 
			
		||||
require_once "vendor/autoload.php";
 | 
			
		||||
 | 
			
		||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
 | 
			
		||||
 | 
			
		||||
// ------------------------
 | 
			
		||||
 | 
			
		||||
$userMgr = new UserMgr();
 | 
			
		||||
@ -76,9 +70,7 @@ function login(ReqHandler &$rh, array $params): string
 | 
			
		||||
 | 
			
		||||
    $user = $userMgr->getUser($nickname);
 | 
			
		||||
    if (($user !== null) && $user->checkPassword($password)) {
 | 
			
		||||
        if (session_status() == PHP_SESSION_NONE) {
 | 
			
		||||
            session_start();
 | 
			
		||||
        }
 | 
			
		||||
        session_start();
 | 
			
		||||
        $_SESSION["nickname"] = $nickname;
 | 
			
		||||
        $result = "OK";
 | 
			
		||||
    } else {
 | 
			
		||||
@ -158,11 +150,7 @@ function start_or_continue_test(ReqHandler &$rh, array $params): string
 | 
			
		||||
    if ($groupMgr->doesUserAccessGame($params["gameid"], $user->getNickname())) {
 | 
			
		||||
        $game = $gameMgr->getGame($params["gameid"]);
 | 
			
		||||
        $test = $testMgr->addOrContinueTest($game, $user);
 | 
			
		||||
        if ($test != null) {
 | 
			
		||||
            return $test->getId();
 | 
			
		||||
        } else {
 | 
			
		||||
            return -1;
 | 
			
		||||
        }
 | 
			
		||||
        return $test->getId();
 | 
			
		||||
    } else {
 | 
			
		||||
        return -1;
 | 
			
		||||
    }
 | 
			
		||||
@ -245,11 +233,10 @@ function access_test_data(string $testid): Test|null
 | 
			
		||||
    return $test;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function exclude_correct_answers_and_comments(array &$tasks): void
 | 
			
		||||
function exclude_correct_answers(array &$challenges): void
 | 
			
		||||
{
 | 
			
		||||
    foreach ($tasks as &$task) {
 | 
			
		||||
        $task["correct_answer"] = null;
 | 
			
		||||
        $task["comment"] = null;
 | 
			
		||||
    foreach ($challenges as &$challenge) {
 | 
			
		||||
        $challenge["correct_answer"] = -1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -259,9 +246,9 @@ function get_player_test(ReqHandler &$rh, array $params): array
 | 
			
		||||
    $test = access_test_data($params["testid"]);
 | 
			
		||||
 | 
			
		||||
    if ($test !== null) {
 | 
			
		||||
        $test_data_with_current_time = $test->toArray(mode: "public");
 | 
			
		||||
        $test_data_with_current_time = $test->toArray();
 | 
			
		||||
        if ($test->isOngoing()) {
 | 
			
		||||
            exclude_correct_answers_and_comments($test_data_with_current_time["challenges"]);
 | 
			
		||||
            exclude_correct_answers($test_data_with_current_time["challenges"]);
 | 
			
		||||
        }
 | 
			
		||||
        $test_data_with_current_time["current_time"] = time();
 | 
			
		||||
        $result = $test_data_with_current_time;
 | 
			
		||||
@ -274,7 +261,7 @@ function save_player_answer(ReqHandler &$rh, array $params): string
 | 
			
		||||
{
 | 
			
		||||
    $test = access_test_data($params["testid"]);
 | 
			
		||||
    if ($test !== null) {
 | 
			
		||||
        $test->saveAnswer($params["task_index"], $params["answer"]);
 | 
			
		||||
        $test->saveAnswer($params["challenge_index"], $params["answer_index"]);
 | 
			
		||||
        return "OK";
 | 
			
		||||
    } else {
 | 
			
		||||
        return "FAIL";
 | 
			
		||||
@ -321,7 +308,7 @@ function get_image(ReqHandler &$rh, array $params): string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$rh->add("get_test", ["testid"], PRIVILEGE_PLAYER, "get_player_test", RESP_JSON, "Get player's test by ID.");
 | 
			
		||||
$rh->add("save_answer", ["testid", "task_index", "answer"], PRIVILEGE_PLAYER, "save_player_answer", RESP_PLAIN, "Store player's answer.");
 | 
			
		||||
$rh->add("save_answer", ["testid", "challenge_index", "answer_index"], PRIVILEGE_PLAYER, "save_player_answer", RESP_PLAIN, "Store player's answer.");
 | 
			
		||||
$rh->add("submit_test", ["testid"], PRIVILEGE_PLAYER, "submit_test", RESP_PLAIN, "Finish player's test.");
 | 
			
		||||
$rh->add("get_image", ["gameid", "img_url"], PRIVILEGE_PLAYER, "get_image", RESP_NONE, "Get image per game.");
 | 
			
		||||
 | 
			
		||||
@ -389,59 +376,39 @@ function create_update_game(ReqHandler &$rh, array $params): array
 | 
			
		||||
 | 
			
		||||
            // update game file if supplied
 | 
			
		||||
            if (isset($_FILES["game_file"])) {
 | 
			
		||||
                // decide whether it's a package or a plain table
 | 
			
		||||
                // decide weather it's a package or a plain table
 | 
			
		||||
                $file = $_FILES["game_file"];
 | 
			
		||||
                $task_import_status = ["n" => 0, "encoding" => "(N/A)"];
 | 
			
		||||
 | 
			
		||||
                // fetch actual and temporary file name
 | 
			
		||||
                $file_name = $file["name"];
 | 
			
		||||
                $file_path = $file["tmp_name"];
 | 
			
		||||
                $challenge_import_status = [];
 | 
			
		||||
 | 
			
		||||
                // determine MIME type
 | 
			
		||||
                $process_once_more = false;
 | 
			
		||||
                do {
 | 
			
		||||
                    // don't process one more time by default
 | 
			
		||||
                    $process_once_more = false;
 | 
			
		||||
                $file_type = strtolower(pathinfo($file["name"], PATHINFO_EXTENSION));
 | 
			
		||||
 | 
			
		||||
                    // get file type
 | 
			
		||||
                    $file_type = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
 | 
			
		||||
                if ($file_type === "zip") { // a package was uploaded
 | 
			
		||||
                    $zip = new ZipArchive;
 | 
			
		||||
                    if ($zip->open($file["tmp_name"])) {
 | 
			
		||||
 | 
			
		||||
                    // extract the archive if a ZIP package was uploaded
 | 
			
		||||
                    if ($file_type === "zip") {
 | 
			
		||||
                        $zip = new ZipArchive;
 | 
			
		||||
                        if ($zip->open($file_path)) {
 | 
			
		||||
                            // get game directory
 | 
			
		||||
                            $game_dir = $game->getGameDir();
 | 
			
		||||
                            //$game_files = glob($game_dir); // get list of existing game files
 | 
			
		||||
 | 
			
		||||
                            // remove former files recursively
 | 
			
		||||
                            $dir_iter = new RecursiveDirectoryIterator($game_dir, FilesystemIterator::SKIP_DOTS);
 | 
			
		||||
                            foreach ($dir_iter as $dir_item) {
 | 
			
		||||
                                $item_path = $dir_item->getPathname();
 | 
			
		||||
                                $dir_item->isDir() ? rmdir($item_path) : unlink($item_path);
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // extract package contents to the game directory
 | 
			
		||||
                            $zip->extractTo($game_dir . DIRECTORY_SEPARATOR);
 | 
			
		||||
 | 
			
		||||
                            // search for the table file
 | 
			
		||||
                            $table_files = glob($game_dir . DIRECTORY_SEPARATOR . "*.{csv,xls,xlsx,ods}", \GLOB_BRACE);
 | 
			
		||||
                            if (count($table_files) > 0) {
 | 
			
		||||
                                $file_name = $table_files[0];
 | 
			
		||||
                                $file_path = $file_name;
 | 
			
		||||
                                $process_once_more = true;
 | 
			
		||||
                            }
 | 
			
		||||
                        $game_dir = $game->getGameDir(); // get game directory
 | 
			
		||||
                        //$game_files = glob($game_dir); // get list of existing game files
 | 
			
		||||
                        // remove former files recursively
 | 
			
		||||
                        $dir_iter = new RecursiveDirectoryIterator($game_dir, FilesystemIterator::SKIP_DOTS);
 | 
			
		||||
                        foreach ($dir_iter as $dir_item) {
 | 
			
		||||
                            $item_path = $dir_item->getPathname();
 | 
			
		||||
                            $dir_item->isDir() ? rmdir($item_path) : unlink($item_path);
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // extract package contents to the game directory
 | 
			
		||||
                        $zip->extractTo($game_dir . DIRECTORY_SEPARATOR);
 | 
			
		||||
 | 
			
		||||
                        // search for the CSV table file
 | 
			
		||||
                        $csv_files = glob($game_dir . DIRECTORY_SEPARATOR . "*.csv") ?? [];
 | 
			
		||||
                        if (count($csv_files) > 0) {
 | 
			
		||||
                            $challenge_import_status = $game->importChallengesFromCSV($csv_files[0]);
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if ($file_type === "csv") { // a plain table was uploaded
 | 
			
		||||
                        $task_import_status = $game->importTasksFromCSV($file_path);
 | 
			
		||||
                    } else if (in_array($file_type, ["xls", "xlsx", "ods"])) {
 | 
			
		||||
                        $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_path, readers: [ucfirst($file_type)]);
 | 
			
		||||
                        $sheet = $spreadsheet->getSheet(0);
 | 
			
		||||
                        $table = $sheet->toArray();
 | 
			
		||||
                        $task_import_status = $game->importTasksFromTable($table);
 | 
			
		||||
                    }
 | 
			
		||||
                } while ($process_once_more);
 | 
			
		||||
                $result = $task_import_status;
 | 
			
		||||
                } else if ($file_type === "csv") { // a plain table was uploaded
 | 
			
		||||
                    $challenge_import_status = $game->importChallengesFromCSV($file["tmp_name"]);
 | 
			
		||||
                }
 | 
			
		||||
                $result = $challenge_import_status;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -461,7 +428,7 @@ function get_all_game_headers(ReqHandler &$rh, array $params): array
 | 
			
		||||
    return $a;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function get_tasks(ReqHandler &$rh, array $params): string
 | 
			
		||||
function get_challenges(ReqHandler &$rh, array $params): string
 | 
			
		||||
{
 | 
			
		||||
    global $user;
 | 
			
		||||
    global $gameMgr;
 | 
			
		||||
@ -504,7 +471,7 @@ function export_game_file_csv(ReqHandler &$rh, array $params): string
 | 
			
		||||
        $f = tmpfile();
 | 
			
		||||
        header("Content-Type: text/csv");
 | 
			
		||||
        header("Content-Disposition: attachment; filename=\"challenges_$gameid.csv\"\r\n");
 | 
			
		||||
        $game->exportTasksToCSV($f);
 | 
			
		||||
        $game->exportChallengesToCSV($f);
 | 
			
		||||
        fseek($f, 0);
 | 
			
		||||
        fpassthru($f);
 | 
			
		||||
    }
 | 
			
		||||
@ -684,7 +651,7 @@ function delete_tests(ReqHandler &$rh, array $params): string
 | 
			
		||||
 | 
			
		||||
$rh->add(["create_game", "update_game"], ["data"], PRIVILEGE_CREATOR, "create_update_game", RESP_JSON, "Create or update game.");
 | 
			
		||||
$rh->add("get_all_game_headers", [], PRIVILEGE_CREATOR, "get_all_game_headers", RESP_JSON, "Get all game headers.");
 | 
			
		||||
$rh->add("get_tasks", [], PRIVILEGE_CREATOR, "get_tasks", RESP_PLAIN, "Get game tasks.");
 | 
			
		||||
$rh->add("get_challenges", [], PRIVILEGE_CREATOR, "get_challenges", RESP_PLAIN, "Get game challenges.");
 | 
			
		||||
$rh->add("delete_games", ["ids"], PRIVILEGE_CREATOR, "delete_games", RESP_PLAIN, "Delete games.");
 | 
			
		||||
$rh->add("export_game_file_csv", ["gameid"], PRIVILEGE_CREATOR, "export_game_file_csv", RESP_NONE, "Export game CSV file.");
 | 
			
		||||
$rh->add("get_results_by_gameid", ["gameid"], PRIVILEGE_CREATOR, "get_results_by_gameid", RESP_JSON, "Get game results.");
 | 
			
		||||
@ -804,7 +771,7 @@ function create_update_user(ReqHandler &$rh, array $params): string
 | 
			
		||||
 | 
			
		||||
                // password replacement, if requested
 | 
			
		||||
                if ($password !== "") {
 | 
			
		||||
                    $tuser->changePassword($password, "", false);
 | 
			
		||||
                    $tuser->changePassword(password_hash($password, PASSWORD_DEFAULT), "", false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $tuser->storeMods();
 | 
			
		||||
@ -895,17 +862,6 @@ function import_users_from_csv(ReqHandler &$rh, array $params): string
 | 
			
		||||
    return "OK";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function execute_cli_command(ReqHandler &$rh, array $params): string
 | 
			
		||||
{
 | 
			
		||||
    $args = $params["cmd"];
 | 
			
		||||
    $phpargs = "-dxdebug.default_enable=1 -dxdebug.remote_enable=1 -dxdebug.remote_autostart=1 -dxdebug.remote_port=9001 -dxdebug.remote_host=127.0.0.1 -dxdebug.remote_mode=req -dxdebug.idekey=PHPSTORM -dxdebug.mode=debug -dxdebug.discover_client_host=true -dxdebug.start_with_request=yes";
 | 
			
		||||
    $cmdline = "php $phpargs cli_actions.php $args";
 | 
			
		||||
    $resp = "=> " . $cmdline . "\n\n";
 | 
			
		||||
    $resp .= shell_exec($cmdline);
 | 
			
		||||
    //$resp = shell_exec("php -v");
 | 
			
		||||
    return $resp ?? "(null output)";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$rh->add("create_group", ["groupname", "description"], PRIVILEGE_QUIZMASTER, "create_update_group", RESP_PLAIN, "Create group.");
 | 
			
		||||
$rh->add("update_group", ["groupname", "description", "owner", "editors", "id"], PRIVILEGE_QUIZMASTER, "create_update_group", RESP_PLAIN, "Update group.");
 | 
			
		||||
$rh->add("delete_groups", ["ids"], PRIVILEGE_QUIZMASTER, "delete_groups", RESP_PLAIN, "Delete group.");
 | 
			
		||||
@ -920,8 +876,6 @@ $rh->add("get_user_groups", ["nickname"], PRIVILEGE_QUIZMASTER, "get_user_groups
 | 
			
		||||
$rh->add("get_game_groups", ["gameid"], PRIVILEGE_QUIZMASTER, "get_game_groups", RESP_JSON, "Get game's groups.");
 | 
			
		||||
$rh->add("import_users_from_csv", [], PRIVILEGE_QUIZMASTER, "import_users_from_csv", RESP_JSON, "Get all users.");
 | 
			
		||||
 | 
			
		||||
$rh->add("execute_cli_command", ["cmd"], PRIVILEGE_QUIZMASTER, "execute_cli_command", RESP_PLAIN, "Run cli command.");
 | 
			
		||||
 | 
			
		||||
//function test(ReqHandler &$rh, array $params): string
 | 
			
		||||
//{
 | 
			
		||||
//    $usrmgr = new UserMgr();
 | 
			
		||||
 | 
			
		||||
@ -56,11 +56,7 @@ function start_or_continue_test(gameid) {
 | 
			
		||||
    request(req).then(resp => {
 | 
			
		||||
        if (resp.length > 0) // response is non-zero
 | 
			
		||||
        {
 | 
			
		||||
            if (Number(resp) !== -1) {
 | 
			
		||||
                open_test(resp, gameid);
 | 
			
		||||
            } else {
 | 
			
		||||
                alert("A teszt nem indítható el!");
 | 
			
		||||
            }
 | 
			
		||||
            open_test(resp, gameid);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@ -88,7 +84,7 @@ function list_corresponding_results(gameid) {
 | 
			
		||||
           let test_summary_record = document.createElement("section");
 | 
			
		||||
           test_summary_record.classList.add("test-summary-record");
 | 
			
		||||
           test_summary_record.addEventListener("click", () => {
 | 
			
		||||
              open_test(record["testid"], gameid);
 | 
			
		||||
              open_test(record["testid"]);
 | 
			
		||||
           });
 | 
			
		||||
 | 
			
		||||
           let sequence_number_sec = document.createElement("section");
 | 
			
		||||
 | 
			
		||||
@ -59,10 +59,10 @@ function create_edit_game(game = null) {
 | 
			
		||||
    let ownerF = document.getElementById("game_owner");
 | 
			
		||||
    let contributorsF = document.getElementById("game_contributors");
 | 
			
		||||
    let gameFileF = document.getElementById("game_file");
 | 
			
		||||
    let download_tasks_btn = document.getElementById("download_tasks_btn");
 | 
			
		||||
    let download_challenges_btn = document.getElementById("download_challenges_btn");
 | 
			
		||||
    let show_game_file_upload_btn = document.getElementById("show_game_file_upload");
 | 
			
		||||
    let cancel_game_file_upload_btn = document.getElementById("cancel_game_file_upload");
 | 
			
		||||
    let edit_tasks_btn = document.getElementById("edit_tasks_btn");
 | 
			
		||||
    let edit_challenges_btn = document.getElementById("edit_challenges_btn");
 | 
			
		||||
    let groupF = document.getElementById("game_groups");
 | 
			
		||||
    let time_limitedChk = document.getElementById("time_limited");
 | 
			
		||||
    let time_limitF = document.getElementById("time_limit");
 | 
			
		||||
@ -136,14 +136,14 @@ function create_edit_game(game = null) {
 | 
			
		||||
 | 
			
		||||
    let game_file_present = updating && game["game_file_present"];
 | 
			
		||||
    if (game_file_present) {
 | 
			
		||||
        show(download_tasks_btn);
 | 
			
		||||
        show(edit_tasks_btn);
 | 
			
		||||
        edit_tasks_btn.onclick = () => {
 | 
			
		||||
            edit_tasks(game);
 | 
			
		||||
        show(download_challenges_btn);
 | 
			
		||||
        show(edit_challenges_btn);
 | 
			
		||||
        edit_challenges_btn.onclick = () => {
 | 
			
		||||
            edit_challenges(game);
 | 
			
		||||
        };
 | 
			
		||||
    } else {
 | 
			
		||||
        hide(download_tasks_btn);
 | 
			
		||||
        hide(edit_tasks_btn);
 | 
			
		||||
        hide(download_challenges_btn);
 | 
			
		||||
        hide(edit_challenges_btn);
 | 
			
		||||
    }
 | 
			
		||||
    show_hide_gamefile_upload(false);
 | 
			
		||||
 | 
			
		||||
@ -206,7 +206,7 @@ function show_hide_gamefile_upload(en) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function download_tasks() {
 | 
			
		||||
function download_challenges() {
 | 
			
		||||
    let action = "export_game_file_csv";
 | 
			
		||||
    let gameid = EDITED_GAME["_id"];
 | 
			
		||||
    window.open(`interface.php?action=${action}&gameid=${gameid}`, "_blank");
 | 
			
		||||
@ -251,8 +251,8 @@ function handle_time_limit_chkbox() {
 | 
			
		||||
    time_limitF.disabled = !time_limitedChk.checked;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function edit_tasks(game) {
 | 
			
		||||
    let req = {action: "get_tasks", gameid: game["_id"]};
 | 
			
		||||
function edit_challenges(game) {
 | 
			
		||||
    let req = {action: "get_challenges", gameid: game["_id"]};
 | 
			
		||||
    request(req).then(resp => {
 | 
			
		||||
        console.log(JSON.parse(resp));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -158,35 +158,35 @@ function generate_report() {
 | 
			
		||||
            report_display.innerHTML = "";
 | 
			
		||||
 | 
			
		||||
            let ch_n = 0;
 | 
			
		||||
            stats.forEach((task) => {
 | 
			
		||||
                let task_box = document.createElement("section");
 | 
			
		||||
                task_box.classList.add("task");
 | 
			
		||||
                task_box.style.width = "100%";
 | 
			
		||||
            stats.forEach((challenge) => {
 | 
			
		||||
                let challenge_box = document.createElement("section");
 | 
			
		||||
                challenge_box.classList.add("challenge");
 | 
			
		||||
                challenge_box.style.width = "100%";
 | 
			
		||||
 | 
			
		||||
                let seq_num = document.createElement("section");
 | 
			
		||||
                seq_num.classList.add("seq-num");
 | 
			
		||||
                seq_num.innerText = ++ch_n;
 | 
			
		||||
                task_box.append(seq_num);
 | 
			
		||||
                challenge_box.append(seq_num);
 | 
			
		||||
 | 
			
		||||
                let img_url = task["image_url"];
 | 
			
		||||
                let img_url = challenge["image_url"];
 | 
			
		||||
                if (img_url !== "") {
 | 
			
		||||
                    let fig = document.createElement("img");
 | 
			
		||||
                    fig.src = `interface.php?action=get_image&gameid=${GAMEID}&img_url=${task["image_url"]}`;
 | 
			
		||||
                    fig.src = `interface.php?action=get_image&gameid=${GAMEID}&img_url=${challenge["image_url"]}`;
 | 
			
		||||
                    fig.classList.add("question-image");
 | 
			
		||||
                    task_box.append(fig);
 | 
			
		||||
                    challenge_box.append(fig);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let question = document.createElement("span");
 | 
			
		||||
                question.classList.add("question");
 | 
			
		||||
                question.innerHTML = preprocess_inserts(task["question"]);
 | 
			
		||||
                question.innerHTML = preprocess_inserts(challenge["question"]);
 | 
			
		||||
                let answer_container = document.createElement("section");
 | 
			
		||||
                answer_container.classList.add("answer-container");
 | 
			
		||||
                task_box.append(question, answer_container);
 | 
			
		||||
                challenge_box.append(question, answer_container);
 | 
			
		||||
 | 
			
		||||
                let n = task["answer_count"];
 | 
			
		||||
                let n = challenge["answer_count"];
 | 
			
		||||
                for (let i = 0; i < n; i++) {
 | 
			
		||||
                    let answer = task["answers"][i];
 | 
			
		||||
                    let correct_answer = answer === task["correct_answer"];
 | 
			
		||||
                    let answer = challenge["answers"][i];
 | 
			
		||||
                    let correct_answer = answer === challenge["correct_answer"];
 | 
			
		||||
 | 
			
		||||
                    let answer_section = document.createElement("section");
 | 
			
		||||
                    answer_section.classList.add("answer");
 | 
			
		||||
@ -196,7 +196,7 @@ function generate_report() {
 | 
			
		||||
                    let progress_bar_indicator = document.createElement("section");
 | 
			
		||||
                    progress_bar_indicator.classList.add("pb-indicator")
 | 
			
		||||
 | 
			
		||||
                    let percentage = task["answer_ratio"][i] * 100;
 | 
			
		||||
                    let percentage = challenge["answer_ratio"][i] * 100;
 | 
			
		||||
                    progress_bar_indicator.style.width = `${percentage}%`;
 | 
			
		||||
                    progress_bar_indicator.innerText = Math.round(percentage * 100.0) / 100.0 + "%";
 | 
			
		||||
                    progress_bar_indicator.setAttribute("correct", correct_answer ? "true" : "false");
 | 
			
		||||
@ -213,7 +213,7 @@ function generate_report() {
 | 
			
		||||
                    answer_container.append(answer_section);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                report_display.append(task_box);
 | 
			
		||||
                report_display.append(challenge_box);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            statsTab.MathJax.typeset();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1066
									
								
								js/tasks.js
									
									
									
									
									
								
							
							
						
						
									
										1066
									
								
								js/tasks.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,17 +0,0 @@
 | 
			
		||||
function submit_command() {
 | 
			
		||||
    let terminal_input = document.getElementById('terminal_input');
 | 
			
		||||
    let terminal_output = document.getElementById('terminal_output');
 | 
			
		||||
 | 
			
		||||
    let cmd = terminal_input.value.trim();
 | 
			
		||||
    terminal_input.disabled = true;
 | 
			
		||||
    if (cmd !== "") {
 | 
			
		||||
        terminal_output.value += ">> " + cmd + "\n";
 | 
			
		||||
        let req = {"action": "execute_cli_command", "cmd" : cmd};
 | 
			
		||||
        request(req).then((resp) => {
 | 
			
		||||
            terminal_output.value += resp + "\n\n";
 | 
			
		||||
            terminal_output.scrollTo(0, terminal_output.scrollHeight);
 | 
			
		||||
            //terminal_input.value = "";
 | 
			
		||||
            terminal_input.disabled = false;
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										208
									
								
								js/testground.js
									
									
									
									
									
								
							
							
						
						
									
										208
									
								
								js/testground.js
									
									
									
									
									
								
							@ -20,9 +20,9 @@ function populate_infobox(test_data, view_only) {
 | 
			
		||||
    if (test_concluded) {
 | 
			
		||||
        let summary = test_data["summary"];
 | 
			
		||||
        let correct_answer_n = summary["correct_answer_n"];
 | 
			
		||||
        let task_n = summary["challenge_n"];
 | 
			
		||||
        let r = Math.ceil((correct_answer_n / task_n) * 100);
 | 
			
		||||
        percentageS.innerHTML = `${r}% (${correct_answer_n}/${task_n})`;
 | 
			
		||||
        let challenge_n = summary["challenge_n"];
 | 
			
		||||
        let r = Math.ceil((correct_answer_n / challenge_n) * 100);
 | 
			
		||||
        percentageS.innerHTML = `${r}% (${correct_answer_n}/${challenge_n})`;
 | 
			
		||||
 | 
			
		||||
        let start_time = unix_time_to_human_readable(test_data["start_time"]);
 | 
			
		||||
        let end_time = unix_time_to_human_readable(test_data["end_time"]);
 | 
			
		||||
@ -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"], false);
 | 
			
		||||
                        populate_all(test_data["_id"], test_data["gameid"]);
 | 
			
		||||
                        clearInterval(INTERVAL_HANDLE);
 | 
			
		||||
                        INTERVAL_HANDLE = null;
 | 
			
		||||
                    }
 | 
			
		||||
@ -70,27 +70,87 @@ function populate_infobox(test_data, view_only) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function populate_tasks(tasks, concluded, view_only = false, gameid) {
 | 
			
		||||
function assemble_answer_radio_id(challenge_N, answer_N) {
 | 
			
		||||
    return challenge_N + "_" + answer_N;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function mark_answers(challenges, view_only = false) {
 | 
			
		||||
    for (let i = 0; i < challenges.length; i++) {
 | 
			
		||||
       let marked_answerR = document.getElementById(assemble_answer_radio_id(i, challenges[i]["player_answer"]));
 | 
			
		||||
       if (marked_answerR !== null) {
 | 
			
		||||
           marked_answerR.checked = true;
 | 
			
		||||
       }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function populate_challenges(challenges, concluded, view_only = false, gameid) {
 | 
			
		||||
    let test_display = document.getElementById("test_display");
 | 
			
		||||
    test_display.innerHTML = "";
 | 
			
		||||
 | 
			
		||||
    Task.sequence_number = 0;
 | 
			
		||||
    let challenge_N = 0;
 | 
			
		||||
    challenges.forEach((challenge) => {
 | 
			
		||||
        let challenge_N_snapshot = challenge_N;
 | 
			
		||||
        let challenge_box = document.createElement("section");
 | 
			
		||||
        challenge_box.classList.add("challenge");
 | 
			
		||||
        let question = document.createElement("span");
 | 
			
		||||
        question.classList.add("question");
 | 
			
		||||
        question.innerHTML = preprocess_inserts(challenge["question"]);
 | 
			
		||||
        let answer_container = document.createElement("section");
 | 
			
		||||
        answer_container.classList.add("answer-container");
 | 
			
		||||
        challenge_box.append(question, answer_container);
 | 
			
		||||
 | 
			
		||||
    tasks.forEach((task) => {
 | 
			
		||||
        let task_element = document.createElement(`${task["type"]}-task`);
 | 
			
		||||
        task_element.uploadAnswerCb = save_answer;
 | 
			
		||||
 | 
			
		||||
        task_element.imgType = task["image_type"];
 | 
			
		||||
        if (task["image_type"] === "url" && task["image_data"] !== "") {
 | 
			
		||||
            task_element.imgData = `interface.php?action=get_image&gameid=${gameid}&img_url=${task["image_data"]}`
 | 
			
		||||
        } else {
 | 
			
		||||
            task_element.imgData = task["image_data"];
 | 
			
		||||
        if (challenge["image_url"] !== "") {
 | 
			
		||||
            let qimg = document.createElement("img");
 | 
			
		||||
            qimg.src = `interface.php?action=get_image&gameid=${gameid}&img_url=${challenge["image_url"]}`;
 | 
			
		||||
            qimg.classList.add("question-image")
 | 
			
		||||
            challenge_box.insertBefore(qimg, answer_container);
 | 
			
		||||
        }
 | 
			
		||||
        task_element.isConcluded = concluded;
 | 
			
		||||
        task_element.isViewOnly = view_only;
 | 
			
		||||
        task_element.fromArray(task);
 | 
			
		||||
        test_display.appendChild(task_element);
 | 
			
		||||
 | 
			
		||||
        let seq_num_section = document.createElement("section");
 | 
			
		||||
        seq_num_section.innerText = String(challenge_N + 1) + "."
 | 
			
		||||
        seq_num_section.classList.add("seq-num");
 | 
			
		||||
        challenge_box.append(seq_num_section);
 | 
			
		||||
 | 
			
		||||
        let answer_N = 0;
 | 
			
		||||
        let player_answer = challenge["player_answer"];
 | 
			
		||||
        player_answer = (player_answer !== "") ? Number(player_answer) : -1;
 | 
			
		||||
        challenge["answers"].forEach((answer) => {
 | 
			
		||||
            let answer_section = document.createElement("section");
 | 
			
		||||
            answer_section.classList.add("answer");
 | 
			
		||||
            let answer_radio = document.createElement("input");
 | 
			
		||||
            answer_radio.type = "radio";
 | 
			
		||||
            answer_radio.id = `${challenge_N}_${answer_N}`;
 | 
			
		||||
            answer_radio.name = `challenge_${challenge_N}`;
 | 
			
		||||
            answer_radio.disabled = concluded || view_only;
 | 
			
		||||
            let answer_N_snapshot = answer_N;
 | 
			
		||||
            answer_radio.addEventListener("input", () => {
 | 
			
		||||
                save_answer(challenge_N_snapshot, answer_N_snapshot);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            let answer_text = document.createElement("label");
 | 
			
		||||
            answer_text.innerHTML = preprocess_inserts(answer);
 | 
			
		||||
            answer_text.setAttribute("for", answer_radio.id);
 | 
			
		||||
            if (concluded && (challenge["correct_answer"] === answer_N)) {
 | 
			
		||||
                answer_text.classList.add("correct-answer")
 | 
			
		||||
 | 
			
		||||
                if (player_answer !== challenge["correct_answer"]) {
 | 
			
		||||
                    challenge_box.classList.add("bad-answer");
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            answer_section.append(answer_radio, answer_text);
 | 
			
		||||
            answer_container.appendChild(answer_section);
 | 
			
		||||
 | 
			
		||||
            answer_N++;
 | 
			
		||||
        });
 | 
			
		||||
        challenge_N++;
 | 
			
		||||
 | 
			
		||||
        test_display.appendChild(challenge_box);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    mark_answers(challenges, view_only);
 | 
			
		||||
 | 
			
		||||
    MathJax.typeset();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function populate_all(test_id, gameid, view_only) {
 | 
			
		||||
@ -102,125 +162,27 @@ function populate_all(test_id, gameid, view_only) {
 | 
			
		||||
    request(req).then(resp => {
 | 
			
		||||
        TEST_DATA = JSON.parse(resp);
 | 
			
		||||
        let concluded = TEST_DATA["state"] === "concluded";
 | 
			
		||||
        populate_tasks(TEST_DATA["challenges"], concluded, view_only, gameid);
 | 
			
		||||
        populate_challenges(TEST_DATA["challenges"], concluded, view_only, gameid);
 | 
			
		||||
        populate_infobox(TEST_DATA, view_only);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function save_answer(tidx, ans) {
 | 
			
		||||
function save_answer(chidx, aidx) {
 | 
			
		||||
    let req = {
 | 
			
		||||
        action: "save_answer",
 | 
			
		||||
        testid: TEST_DATA["_id"],
 | 
			
		||||
        task_index: tidx,
 | 
			
		||||
        answer: ans,
 | 
			
		||||
        challenge_index: chidx,
 | 
			
		||||
        answer_index: aidx,
 | 
			
		||||
    };
 | 
			
		||||
    request(req);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function save_all_answers() {
 | 
			
		||||
    let tasks = document.getElementById("test_display").children;
 | 
			
		||||
    for (let i = 0; i < tasks.length; i++) {
 | 
			
		||||
        tasks[i].uploadAnswer();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function submit_test() {
 | 
			
		||||
    // first, save all answers
 | 
			
		||||
    save_all_answers();
 | 
			
		||||
 | 
			
		||||
    // then signal test submission
 | 
			
		||||
    let req = {
 | 
			
		||||
        action: "submit_test",
 | 
			
		||||
        testid: TEST_DATA["_id"]
 | 
			
		||||
    }
 | 
			
		||||
    request(req).then(resp => {
 | 
			
		||||
        populate_all(TEST_DATA["_id"], TEST_DATA["gameid"], false);
 | 
			
		||||
        populate_all(TEST_DATA["_id"], TEST_DATA["gameid"]);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------
 | 
			
		||||
 | 
			
		||||
// function populate_tasks(tasks, concluded, view_only = false, gameid) {
 | 
			
		||||
//     let test_display = document.getElementById("test_display");
 | 
			
		||||
//     test_display.innerHTML = "";
 | 
			
		||||
//
 | 
			
		||||
//     let task_N = 0;
 | 
			
		||||
//     tasks.forEach((task) => {
 | 
			
		||||
//         let task_N_snapshot = task_N;
 | 
			
		||||
//         let task_box = document.createElement("section");
 | 
			
		||||
//         task_box.classList.add("task");
 | 
			
		||||
//         let question = document.createElement("span");
 | 
			
		||||
//         question.classList.add("question");
 | 
			
		||||
//         question.innerHTML = preprocess_inserts(task["question"]);
 | 
			
		||||
//         let answer_container = document.createElement("section");
 | 
			
		||||
//         answer_container.classList.add("answer-container");
 | 
			
		||||
//         task_box.append(question, answer_container);
 | 
			
		||||
//
 | 
			
		||||
//         if (task["image_url"] !== "") {
 | 
			
		||||
//             let qimg = document.createElement("img");
 | 
			
		||||
//             qimg.src = `interface.php?action=get_image&gameid=${gameid}&img_url=${task["image_url"]}`;
 | 
			
		||||
//             qimg.classList.add("question-image")
 | 
			
		||||
//             task_box.insertBefore(qimg, answer_container);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         let seq_num_section = document.createElement("section");
 | 
			
		||||
//         seq_num_section.innerText = String(task_N + 1) + "."
 | 
			
		||||
//         seq_num_section.classList.add("seq-num");
 | 
			
		||||
//         task_box.append(seq_num_section);
 | 
			
		||||
//
 | 
			
		||||
//         let answer_N = 0;
 | 
			
		||||
//         let player_answer = task["player_answer"];
 | 
			
		||||
//         player_answer = (player_answer !== "") ? Number(player_answer) : -1;
 | 
			
		||||
//         task["answers"].forEach((answer) => {
 | 
			
		||||
//             let answer_section = document.createElement("section");
 | 
			
		||||
//             answer_section.classList.add("answer");
 | 
			
		||||
//             let answer_radio = document.createElement("input");
 | 
			
		||||
//             answer_radio.type = "radio";
 | 
			
		||||
//             answer_radio.id = `${task_N}_${answer_N}`;
 | 
			
		||||
//             answer_radio.name = `task_${task_N}`;
 | 
			
		||||
//             answer_radio.disabled = concluded || view_only;
 | 
			
		||||
//             let answer_N_snapshot = answer_N;
 | 
			
		||||
//             answer_radio.addEventListener("input", () => {
 | 
			
		||||
//                 save_answer(task_N_snapshot, answer_N_snapshot);
 | 
			
		||||
//             });
 | 
			
		||||
//
 | 
			
		||||
//             let answer_text = document.createElement("label");
 | 
			
		||||
//             answer_text.innerHTML = preprocess_inserts(answer);
 | 
			
		||||
//             answer_text.setAttribute("for", answer_radio.id);
 | 
			
		||||
//             if (concluded && (task["correct_answer"] === answer_N)) {
 | 
			
		||||
//                 answer_text.classList.add("correct-answer")
 | 
			
		||||
//
 | 
			
		||||
//                 if (player_answer !== task["correct_answer"]) {
 | 
			
		||||
//                     task_box.classList.add("bad-answer");
 | 
			
		||||
//                 }
 | 
			
		||||
//             }
 | 
			
		||||
//
 | 
			
		||||
//             answer_section.append(answer_radio, answer_text);
 | 
			
		||||
//             answer_container.appendChild(answer_section);
 | 
			
		||||
//
 | 
			
		||||
//             answer_N++;
 | 
			
		||||
//         });
 | 
			
		||||
//         task_N++;
 | 
			
		||||
//
 | 
			
		||||
//         test_display.appendChild(task_box);
 | 
			
		||||
//     });
 | 
			
		||||
//
 | 
			
		||||
//     mark_answers(tasks, view_only);
 | 
			
		||||
//
 | 
			
		||||
//     MathJax.typeset();
 | 
			
		||||
// }
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// function assemble_answer_radio_id(task_N, answer_N) {
 | 
			
		||||
//     return task_N + "_" + answer_N;
 | 
			
		||||
// }
 | 
			
		||||
//
 | 
			
		||||
//
 | 
			
		||||
// function mark_answers(tasks, view_only = false) {
 | 
			
		||||
//     for (let i = 0; i < tasks.length; i++) {
 | 
			
		||||
//         let marked_answerR = document.getElementById(assemble_answer_radio_id(i, tasks[i]["player_answer"]));
 | 
			
		||||
//         if (marked_answerR !== null) {
 | 
			
		||||
//             marked_answerR.checked = true;
 | 
			
		||||
//         }
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
}
 | 
			
		||||
@ -83,7 +83,7 @@ function create_edit_user(user = null) {
 | 
			
		||||
        nicknameF.value = user["nickname"];
 | 
			
		||||
        nicknameF.readOnly = true;
 | 
			
		||||
        realnameF.value = user["realname"];
 | 
			
		||||
        passwordF.task_type = "password";
 | 
			
		||||
        passwordF.type = "password";
 | 
			
		||||
        passwordF.value = "";
 | 
			
		||||
        passwordF.readOnly = false;
 | 
			
		||||
        groupsF.value = "";
 | 
			
		||||
@ -97,7 +97,7 @@ function create_edit_user(user = null) {
 | 
			
		||||
        nicknameF.value = "";
 | 
			
		||||
        nicknameF.readOnly = false;
 | 
			
		||||
        realnameF.value = "";
 | 
			
		||||
        passwordF.task_type = "text";
 | 
			
		||||
        passwordF.type = "text";
 | 
			
		||||
        passwordF.value = generateRandomString();
 | 
			
		||||
        passwordF.readOnly = true;
 | 
			
		||||
        groupsF.value = "";
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								main.php
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								main.php
									
									
									
									
									
								
							@ -29,15 +29,8 @@ $privilege = $user_data["privilege"];
 | 
			
		||||
        <script src="js/req.js"></script>
 | 
			
		||||
        <script src="js/main.js"></script>
 | 
			
		||||
        <script src="js/spreadquiz.js"></script>
 | 
			
		||||
        <?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
 | 
			
		||||
            <script src="js/quizmaster_common.js"></script>
 | 
			
		||||
            <script src="js/terminal.js"></script>
 | 
			
		||||
        <?php } ?>
 | 
			
		||||
        <link rel="stylesheet" href="style/spreadquiz.css"/>
 | 
			
		||||
        <link rel="stylesheet" href="style/spreadquiz_mobile.css"/>
 | 
			
		||||
        <?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
 | 
			
		||||
            <link rel="stylesheet" href="style/quizmaster_area.css"/>
 | 
			
		||||
        <?php } ?>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
    <section id="screen_panel">
 | 
			
		||||
@ -59,37 +52,16 @@ $privilege = $user_data["privilege"];
 | 
			
		||||
                <section id="action_panel" class="info-pane-element">
 | 
			
		||||
                    <?php if (($privilege === PRIVILEGE_CREATOR) || ($privilege === PRIVILEGE_QUIZMASTER)) { ?>
 | 
			
		||||
                        <input type="button" value="Nyitólap" onclick="open_in_content_frame('default_frame.php')">
 | 
			
		||||
                        <input type="button" value="Tartalmak kezelése"
 | 
			
		||||
                               onclick="open_in_content_frame('game_manager_frame.php')">
 | 
			
		||||
                        <input type="button" value="Tartalmak kezelése" onclick="open_in_content_frame('game_manager_frame.php')">
 | 
			
		||||
                    <?php } ?>
 | 
			
		||||
                    <?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
 | 
			
		||||
                        <input type="button" value="Felhasználók kezelése"
 | 
			
		||||
                               onclick="open_in_content_frame('user_manager_frame.php')">
 | 
			
		||||
                        <input type="button" value="Csoportok kezelése"
 | 
			
		||||
                               onclick="open_in_content_frame('group_manager_frame.php')">
 | 
			
		||||
                        <input type="button" value="Terminál" onclick="show('terminal_window')">
 | 
			
		||||
                        <input type="button" value="Felhasználók kezelése" onclick="open_in_content_frame('user_manager_frame.php')">
 | 
			
		||||
                        <input type="button" value="Csoportok kezelése" onclick="open_in_content_frame('group_manager_frame.php')">
 | 
			
		||||
                    <?php } ?>
 | 
			
		||||
                </section>
 | 
			
		||||
            <?php } ?>
 | 
			
		||||
        </section>
 | 
			
		||||
    </section>
 | 
			
		||||
    <?php if ($privilege === PRIVILEGE_QUIZMASTER) { ?>
 | 
			
		||||
        <section class="window" shown="false" id="terminal_window">
 | 
			
		||||
            <section class="window-inner">
 | 
			
		||||
                <textarea class="terminal-style" style="height: 20em;" readonly placeholder="(kimenet)" id="terminal_output"></textarea><br>
 | 
			
		||||
                <input type="text" class="terminal-style" id="terminal_input" placeholder="(parancs)"><br>
 | 
			
		||||
                <input type="button" onclick="hide('terminal_window')" value="Bezárás">
 | 
			
		||||
                <script>
 | 
			
		||||
                    let term_input = document.getElementById("terminal_input");
 | 
			
		||||
                    term_input.addEventListener("keypress", function (e) {
 | 
			
		||||
                       if (e.key === "Enter") {
 | 
			
		||||
                           submit_command();
 | 
			
		||||
                       }
 | 
			
		||||
                    });
 | 
			
		||||
                </script>
 | 
			
		||||
            </section>
 | 
			
		||||
        </section>
 | 
			
		||||
    <?php } ?>
 | 
			
		||||
    <section class="window" shown="false" id="change_password_window">
 | 
			
		||||
        <section class="window-inner">
 | 
			
		||||
            <section>
 | 
			
		||||
 | 
			
		||||
@ -5,9 +5,7 @@
 | 
			
		||||
    <title>SpreadQuiz :: Karbantartás</title>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<section style="margin: 0 auto; padding-top: 10ex; text-align: center">
 | 
			
		||||
    <img src="media/maintenance.png" style="width: 16vw">
 | 
			
		||||
    <h3 style="font-family: 'Courier New', monospace"> Az oldal karbantartás alatt áll!</h3>
 | 
			
		||||
</section>
 | 
			
		||||
<img src="media/maintenance.png" width="180">
 | 
			
		||||
<h3> Az oldal karbantartás alatt áll!</h3>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@ -10,7 +10,6 @@
 | 
			
		||||
    <script src="js/common.js"></script>
 | 
			
		||||
    <link rel="stylesheet" href="style/spreadquiz.css">
 | 
			
		||||
    <link rel="stylesheet" href="style/quizmaster_area.css"/>
 | 
			
		||||
    <link rel="stylesheet" href="style/report.css"/>
 | 
			
		||||
    <script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
@ -47,9 +47,9 @@ if (!$gameMgr->getGame($game_id)->isUserContributorOrOwner($user_data["nickname"
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
<section style="margin-bottom: 0.3em">
 | 
			
		||||
    <input type="text" placeholder="Szűrőfeltétel" id="filter" style="font-family: 'Source Code Pro', monospace; width: 50em;">
 | 
			
		||||
    <input type="text" placeholder="Csoportok" id="groups" style="font-family: 'Source Code Pro', monospace; width: 30em;">
 | 
			
		||||
    <input type="text" placeholder="Rendezés" id="orderby" style="font-family: 'Source Code Pro', monospace; width: 30em;">
 | 
			
		||||
    <input type="text" placeholder="Szűrőfeltétel" id="filter" style="font-family: 'Monaco', monospace; width: 50em;">
 | 
			
		||||
    <input type="text" placeholder="Csoportok" id="groups" style="font-family: 'Monaco', monospace; width: 30em;">
 | 
			
		||||
    <input type="text" placeholder="Rendezés" id="orderby" style="font-family: 'Monaco', monospace; width: 30em;">
 | 
			
		||||
    <input type="button" value="Szűrés" onclick="fetch_results()">
 | 
			
		||||
    <input type="button" value="Jelentés előállítása" onclick="generate_report()">
 | 
			
		||||
    <input type="button" value="Kijelöltek törlése" onclick="delete_tests()"><br>
 | 
			
		||||
 | 
			
		||||
@ -132,10 +132,4 @@ span.answer[correct=true] {
 | 
			
		||||
    color: whitesmoke;
 | 
			
		||||
    background-color: #176767;
 | 
			
		||||
    border-radius: 0.3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.terminal-style {
 | 
			
		||||
    font-family: 'Source Code Pro', monospace;
 | 
			
		||||
    width: 40em;
 | 
			
		||||
    font-size: 12pt;
 | 
			
		||||
}
 | 
			
		||||
@ -1,74 +0,0 @@
 | 
			
		||||
/* FIXME: ezek mind át lettek téve a task custom-elementbe */
 | 
			
		||||
 | 
			
		||||
section.task {
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    width: 40em;
 | 
			
		||||
    /*border: 1px solid black;*/
 | 
			
		||||
    padding: 1em;
 | 
			
		||||
    background-color: #d3e5e5;
 | 
			
		||||
    margin-bottom: 0.5em;
 | 
			
		||||
    border-radius: 0.3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
span.question {
 | 
			
		||||
    font-size: 1.3em;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    color: #176767;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer-container {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer {
 | 
			
		||||
    margin: 0.3em 0.8em;
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer label {
 | 
			
		||||
    margin-left: 0.5em;
 | 
			
		||||
    padding: 0.3em 0.5em;
 | 
			
		||||
    border-radius: 0.3em;
 | 
			
		||||
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    max-width: 85%;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer label.correct-answer {
 | 
			
		||||
    border: 2px solid #176767 !important;
 | 
			
		||||
    background-color: #176767;
 | 
			
		||||
    color: whitesmoke;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer input[type="radio"]:checked+label:not(.correct-answer) {
 | 
			
		||||
    background-color: #176767;
 | 
			
		||||
    color: whitesmoke;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
code {
 | 
			
		||||
    font-family: 'Source Code Pro', monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
img.question-image {
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    margin: 1em auto;
 | 
			
		||||
    border-radius: 0.3em;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.seq-num {
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    padding: 0.5em;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    background-color: #176767;
 | 
			
		||||
    color: whitesmoke;
 | 
			
		||||
    border-bottom-right-radius: 0.3em;
 | 
			
		||||
    border-top-left-radius: 0.3em;
 | 
			
		||||
    width: 2em;
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
@import url('https://fonts.googleapis.com/css2?family=Autour+One&family=Kanit:wght@500&display=swap');
 | 
			
		||||
@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0");
 | 
			
		||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap');
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    font-family: 'Autour One', sans-serif;
 | 
			
		||||
@ -230,6 +229,55 @@ section#test_area {
 | 
			
		||||
    right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.challenge {
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    width: 40em;
 | 
			
		||||
    /*border: 1px solid black;*/
 | 
			
		||||
    padding: 1em;
 | 
			
		||||
    background-color: #d3e5e5;
 | 
			
		||||
    margin-bottom: 0.5em;
 | 
			
		||||
    border-radius: 0.3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
span.question {
 | 
			
		||||
    font-size: 1.3em;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
    color: #176767;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer-container {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer {
 | 
			
		||||
    margin: 0.3em 0.8em;
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer label {
 | 
			
		||||
    margin-left: 0.5em;
 | 
			
		||||
    padding: 0.3em 0.5em;
 | 
			
		||||
    border-radius: 0.3em;
 | 
			
		||||
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    max-width: 85%;
 | 
			
		||||
    vertical-align: middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer label.correct-answer {
 | 
			
		||||
    border: 2px solid #176767 !important;
 | 
			
		||||
    background-color: #176767;
 | 
			
		||||
    color: whitesmoke;
 | 
			
		||||
    /*padding: 0.1em;*/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.answer input[type="radio"]:checked+label:not(.correct-answer) {
 | 
			
		||||
    background-color: #176767;
 | 
			
		||||
    color: whitesmoke;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section#infobox {
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: fixed;
 | 
			
		||||
@ -320,6 +368,39 @@ section.test-summary-record {
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
code {
 | 
			
		||||
    font-family: 'Monaco', monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
img.question-image {
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    margin: 1em auto;
 | 
			
		||||
    border-radius: 0.3em;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.seq-num {
 | 
			
		||||
    display: block;
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 0;
 | 
			
		||||
    padding: 0.5em;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    background-color: #176767;
 | 
			
		||||
    color: whitesmoke;
 | 
			
		||||
    border-bottom-right-radius: 0.3em;
 | 
			
		||||
    border-top-left-radius: 0.3em;
 | 
			
		||||
    width: 2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.bad-answer {
 | 
			
		||||
    background-color: #e5d8d3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section.bad-answer section.answer input[type="radio"]:checked+label:not(.correct-answer) {
 | 
			
		||||
    background-color: #aa8a7d;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
section#further-info {
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
    padding: 0.4em 0;
 | 
			
		||||
 | 
			
		||||
@ -34,15 +34,13 @@
 | 
			
		||||
        height: 12em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*!* FIXME: áttéve *!*/
 | 
			
		||||
    /*section.task {*/
 | 
			
		||||
    /*    width: calc(100vw - 3em);*/
 | 
			
		||||
    /*}*/
 | 
			
		||||
    section.challenge {
 | 
			
		||||
        width: calc(100vw - 3em);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*!* FIXME: áttéve *!*/
 | 
			
		||||
    /*section.answer label {*/
 | 
			
		||||
    /*    max-width: 80%;*/
 | 
			
		||||
    /*}*/
 | 
			
		||||
    section.answer label {
 | 
			
		||||
        max-width: 80%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    section#infobox {
 | 
			
		||||
        top: unset;
 | 
			
		||||
 | 
			
		||||
@ -30,16 +30,9 @@ if ($testid === "") {
 | 
			
		||||
    <script src="js/o.js"></script>
 | 
			
		||||
    <script src="js/common.js"></script>
 | 
			
		||||
    <script src="js/testground.js"></script>
 | 
			
		||||
    <script src="js/tasks.js"></script>
 | 
			
		||||
    <link rel="stylesheet" href="style/spreadquiz.css">
 | 
			
		||||
    <link rel="stylesheet" href="style/spreadquiz_mobile.css">
 | 
			
		||||
    <script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="https://nturley.github.io/netlistsvg/elk.bundled.js"></script>
 | 
			
		||||
    <script type="text/javascript" src="https://nturley.github.io/netlistsvg/built/netlistsvg.bundle.js"></script>
 | 
			
		||||
    <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.43.3/ace.min.js" integrity="sha512-BHJlu9vUXVrcxhRwbBdNv3uTsbscp8pp3LJ5z/sw9nBJUegkNlkcZnvODRgynJWhXMCsVUGZlFuzTrr5I2X3sQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
 | 
			
		||||
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/base16/google-light.min.css"/>
 | 
			
		||||
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script>
 | 
			
		||||
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/languages/verilog.min.js"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<section id="test_display">
 | 
			
		||||
@ -72,12 +65,7 @@ if ($testid === "") {
 | 
			
		||||
    </section>
 | 
			
		||||
</section>
 | 
			
		||||
<script>
 | 
			
		||||
    populate_all("<?=$testid ?>", "<?=$gameid ?>", <?=$view_only ? "true" : "false" ?>);
 | 
			
		||||
    window.onbeforeunload = () => {
 | 
			
		||||
        if (TEST_DATA["state"] !== "concluded") {
 | 
			
		||||
            save_all_answers();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    populate_all("<?=$testid ?>", "<?=$gameid ?>", <?=$view_only ?>);
 | 
			
		||||
</script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
import sys
 | 
			
		||||
from schemdraw import logic
 | 
			
		||||
from schemdraw.parsing import logicparse
 | 
			
		||||
 | 
			
		||||
if len(sys.argv) < 3:
 | 
			
		||||
    exit(0)
 | 
			
		||||
 | 
			
		||||
network = logicparse(sys.argv[1], outlabel=sys.argv[2], gateH=1.2)
 | 
			
		||||
#network.save(sys.argv[3])
 | 
			
		||||
svg = network.get_imagedata()
 | 
			
		||||
print(svg.decode("utf-8"))
 | 
			
		||||
@ -1,3 +0,0 @@
 | 
			
		||||
source ./workspace/venv/bin/activate
 | 
			
		||||
python "$@"
 | 
			
		||||
deactivate
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user