diff --git a/class/Game.php b/class/Game.php
new file mode 100644
index 0000000..2ab023e
--- /dev/null
+++ b/class/Game.php
@@ -0,0 +1,572 @@
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/class/GameMgr.php b/class/GameMgr.php
index 539689e..5e4ce16 100644
--- a/class/GameMgr.php
+++ b/class/GameMgr.php
@@ -4,351 +4,7 @@ require_once "vendor/autoload.php";
require_once "AutoStoring.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 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;
- }
-
- if ($version < 3) {
-
- return;
-
- //$a["version"] = 3;
- }
- }
-
- // 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::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 challenges to a CSV file. TODO: ez csak a feleletválasztóshoz lesz jó
- 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. TODO: ez csak a feleletválasztós betöltésére lesz jó
- function importChallengesFromCSV(string $csv_path): array
- {
- // convert text encoding into UTF-8
- $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;
- }
-}
+require_once "Game.php";
class GameMgr
{
@@ -383,7 +39,7 @@ class GameMgr
}
function addGame(string $name, string $owner, string $description, array $properties = Game::DEFAULT_GAME_PROPERTIES,
- array $contributors = [], array $challenges = []): bool
+ array $contributors = [], array $tasks = []): bool
{
$game_data = [
"name" => $name,
@@ -403,7 +59,7 @@ class GameMgr
$game = Game::fromArray($this, $game_data);
$current_game_media_dir = $game->getGameDir();
mkdir($current_game_media_dir);
- $game->saveChallenges();
+ $game->saveTasks();
return true;
}
@@ -466,5 +122,22 @@ 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();
+ }
+ }
+
// -------
}
diff --git a/class/Group.php b/class/Group.php
new file mode 100644
index 0000000..c96f4d6
--- /dev/null
+++ b/class/Group.php
@@ -0,0 +1,226 @@
+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));
+ }
+}
\ No newline at end of file
diff --git a/class/GroupMgr.php b/class/GroupMgr.php
index a557dea..f43af6c 100644
--- a/class/GroupMgr.php
+++ b/class/GroupMgr.php
@@ -6,228 +6,7 @@ require_once "AutoStoring.php";
require_once "privilege_levels.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));
- }
-}
+require_once "Group.php";
class GroupMgr
{
diff --git a/class/LogicFunction.php b/class/LogicFunction.php
new file mode 100644
index 0000000..c1ae9cf
--- /dev/null
+++ b/class/LogicFunction.php
@@ -0,0 +1,271 @@
+ !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: ([\/!~]*()[&]{1,2})*([\/!~]*())
+ 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();
\ No newline at end of file
diff --git a/class/LogicUtils.php b/class/LogicUtils.php
new file mode 100644
index 0000000..8ac7614
--- /dev/null
+++ b/class/LogicUtils.php
@@ -0,0 +1,57 @@
+= ($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;
+ }
+}
\ No newline at end of file
diff --git a/class/LuaUtils.php b/class/LuaUtils.php
new file mode 100644
index 0000000..93a0f72
--- /dev/null
+++ b/class/LuaUtils.php
@@ -0,0 +1,51 @@
+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) ],
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/class/PythonUtils.php b/class/PythonUtils.php
new file mode 100644
index 0000000..b10b782
--- /dev/null
+++ b/class/PythonUtils.php
@@ -0,0 +1,19 @@
+ "'$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
+ }
+}
\ No newline at end of file
diff --git a/class/ReportBuilder.php b/class/ReportBuilder.php
index 680cd04..8dd3889 100644
--- a/class/ReportBuilder.php
+++ b/class/ReportBuilder.php
@@ -79,7 +79,7 @@ class Answer
}
}
-class ChallengeReport
+class TaskReport
{
private string $question;
private array $answers;
@@ -143,21 +143,21 @@ class ChallengeReport
class ReportSection
{
private string $title;
- private array $challenges;
+ private array $tasks;
private function getNumberOfSubmissions() : int {
- return count($this->challenges) > 0 ? $this->challenges[0]->getSubmissionCount() : 0;
+ return count($this->tasks) > 0 ? $this->tasks[0]->getSubmissionCount() : 0;
}
- function __construct(string $title, array $challenges)
+ function __construct(string $title, array $tasks)
{
$this->title = $title;
- $this->challenges = array_map(fn($ch) => new ChallengeReport($ch), $challenges);
+ $this->tasks = array_map(fn($ch) => new TaskReport($ch), $tasks);
}
- function getChallenges(): array
+ function getTasks(): array
{
- return $this->challenges;
+ return $this->tasks;
}
function getTitle(): string
@@ -169,8 +169,8 @@ class ReportSection
function genTeX(): string
{
$tex = "\\begin{quiz}{" . $this->title . "}{" . $this->getNumberOfSubmissions() . "}\n";
- foreach ($this->challenges as $challenge) {
- $tex .= $challenge->genTeX();
+ foreach ($this->tasks as $task) {
+ $tex .= $task->genTeX();
}
$tex .= "\\end{quiz}\n";
return $tex;
diff --git a/class/Task.php b/class/Task.php
new file mode 100644
index 0000000..0bd1f25
--- /dev/null
+++ b/class/Task.php
@@ -0,0 +1,309 @@
+ [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 . "
";
+ }
+
+ 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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/class/TaskFactory.php b/class/TaskFactory.php
new file mode 100644
index 0000000..3e7a71f
--- /dev/null
+++ b/class/TaskFactory.php
@@ -0,0 +1,55 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/class/Tasks/LogicFunctionTask.php b/class/Tasks/LogicFunctionTask.php
new file mode 100644
index 0000000..1be87a8
--- /dev/null
+++ b/class/Tasks/LogicFunctionTask.php
@@ -0,0 +1,60 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/class/Tasks/LogicTaskBase.php b/class/Tasks/LogicTaskBase.php
new file mode 100644
index 0000000..d1d945c
--- /dev/null
+++ b/class/Tasks/LogicTaskBase.php
@@ -0,0 +1,107 @@
+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();
+ }
+}
\ No newline at end of file
diff --git a/class/Tasks/NumberConversionTask.php b/class/Tasks/NumberConversionTask.php
new file mode 100644
index 0000000..a376651
--- /dev/null
+++ b/class/Tasks/NumberConversionTask.php
@@ -0,0 +1,133 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/class/Tasks/OpenEndedTask.php b/class/Tasks/OpenEndedTask.php
new file mode 100644
index 0000000..5299fb2
--- /dev/null
+++ b/class/Tasks/OpenEndedTask.php
@@ -0,0 +1,64 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/class/Tasks/PicturedTask.php b/class/Tasks/PicturedTask.php
new file mode 100644
index 0000000..6018dbe
--- /dev/null
+++ b/class/Tasks/PicturedTask.php
@@ -0,0 +1,53 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/class/Tasks/SingleChoiceTask.php b/class/Tasks/SingleChoiceTask.php
new file mode 100644
index 0000000..bfc4488
--- /dev/null
+++ b/class/Tasks/SingleChoiceTask.php
@@ -0,0 +1,92 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/class/Tasks/TruthTableTask.php b/class/Tasks/TruthTableTask.php
new file mode 100644
index 0000000..40f3751
--- /dev/null
+++ b/class/Tasks/TruthTableTask.php
@@ -0,0 +1,42 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/class/Tasks/VerilogTask.php b/class/Tasks/VerilogTask.php
new file mode 100644
index 0000000..6abb9d5
--- /dev/null
+++ b/class/Tasks/VerilogTask.php
@@ -0,0 +1,122 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/class/Test.php b/class/Test.php
new file mode 100644
index 0000000..170cac0
--- /dev/null
+++ b/class/Test.php
@@ -0,0 +1,257 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/class/TestMgr.php b/class/TestMgr.php
index 329a655..1aa3d85 100644
--- a/class/TestMgr.php
+++ b/class/TestMgr.php
@@ -8,534 +8,7 @@ require_once "ExpressionBuilder.php";
require_once "globals.php";
-const TEST_ONGOING = "ongoing";
-const TEST_CONCLUDED = "concluded";
-
-class TestSummary
-{
- public int $maxMark; // Number of challenges
- public int $mark; // Number of correct answers
- private float $percentage; // Ratio of correct answers
-
- // Calculate percentage.
- private function calculatePercentage(): void
- {
- if ($this->maxMark > 0) {
- $this->percentage = $this->mark / (double)$this->maxMark * 100.0;
- } else { // avoid division by zero
- $this->percentage = 0.0;
- }
- }
-
- function __construct(int $challengeN, int $correctAnswerN)
- {
- $this->maxMark = $challengeN;
- $this->mark = $correctAnswerN;
- $this->calculatePercentage();
- }
-
- // Get challenge count.
- function getMaxMark(): int
- {
- return $this->maxMark;
- }
-
- // Get number of correct answers.
- 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];
- }
-}
-
-class Challenge
-{
- protected string $type; // challenge type
-
- protected float $max_mark; // maximum points that can be collected at this challenge
-
- protected bool $is_template; // this challenge is a template
-
- function __construct(string $type)
- {
- $this->type = $type;
- $this->is_template = false;
- $this->max_mark = 1.0;
- }
-
- // save answer
- function saveAnswer(int|string $ans): bool
- {
- return false;
- }
-
- // clear answer
- function clearAnswer(int|string $ans): bool
- {
- return false;
- }
-
- // get challenge type
- function getType(): string
- {
- return $this->type;
- }
-
- function setMaxMark(float $max_mark): void
- {
- $this->max_mark = $max_mark;
- }
-
- function getMaxMark(): float {
- return $this->max_mark;
- }
-
- function getMark(): float
- {
- return 1.0;
- }
-
- function toArray(): array
- {
- return ["type" => $this->type];
- }
-
- function setTemplate(bool $is_template): void
- {
- $this->is_template = $is_template;
- }
-
- function isTemplate(): bool
- {
- return $this->is_template;
- }
-
- function randomize(): void {
- return;
- }
-}
-
-class PicturedChallenge extends Challenge
-{
- protected string $image_url; // the URL of the corresponding image
-
- function __construct(string $type, array $a = null)
- {
- parent::__construct($type);
- $this->image_url = $a["image_url"] ?? "";
- }
-
- function setImageUrl(string $image_url): void
- {
- $this->image_url = $image_url;
- }
-
- function getImageUrl(): string
- {
- return $this->image_url;
- }
-
- function toArray(): array
- {
- $a = parent::toArray();
- $a["image_url"] = $this->image_url;
- return $a;
- }
-}
-
-class SingleChoiceChallenge extends PicturedChallenge
-{
- private string $question; // the task title
- private array $answers; // possible answers
- private int $correct_answer; // the single correct answer
-
- private int $player_answer; // answer given by the player
-
- // -----------------
-
- function __construct(array $a = null)
- {
- parent::__construct("singlechoice", $a);
-
- $this->question = $a["question"] ?? "";
- $this->answers = $a["answers"] ?? [];
- $this->correct_answer = (int)($a["correct_answer"] ?? -1);
- $this->player_answer = (int)($a["player_answer"] ?? -1);
- }
-
- function setQuestion(string $question): void
- {
- $this->question = $question;
- }
-
- function getQuestion(): string
- {
- return $this->question;
- }
-
- function addAnswer(string $answer): void
- {
- $this->answers[] = $answer;
- }
-
- function getAnswers(): array
- {
- return $this->answers;
- }
-
- function setCorrectAnswer(string $correct_answer): void
- {
- $this->correct_answer = $correct_answer;
- }
-
- function getCorrectAnswer(): string
- {
- return $this->correct_answer;
- }
-
- private function isAnswerIdInsideBounds($ansid): bool
- {
- return ($ansid >= 0) && ($ansid <= count($this->answers));
- }
-
- function saveAnswer(int|string $ans): bool
- {
- $ansidx = (int)($ans); // cast answer to integer as it is a number
- if ($this->isAnswerIdInsideBounds($ansidx)) {
- $this->player_answer = $ansidx;
- return true;
- }
-
- return false;
- }
-
- function clearAnswer(int|string $ans): bool
- {
- $ansidx = (int)($ans); // cast answer to integer as it is a number
- if ($this->isAnswerIdInsideBounds($ansidx)) {
- $this->player_answer = -1;
- return true;
- }
-
- return false;
- }
-
- public function getMark(): float
- {
- return ($this->player_answer == $this->correct_answer) ? 1.0 : 0.0;
- }
-
- function toArray(): array
- {
- $a = parent::toArray();
- $a["question"] = $this->question;
- $a["answers"] = $this->answers;
- $a["correct_answer"] = $this->correct_answer;
- if (!$this->isTemplate()) {
- $a["player_answer"] = $this->player_answer;
- }
- return $a;
- }
-
- function randomize(): void{
- //shuffle($this->answers); // shuffle answers
- //$this->correct_answer = array_search($this->correct_answer, $this->answers); // remap correct answer
- }
-}
-
-class ChallengeFactory
-{
- static function fromArray(array $a): Challenge|null
- {
- $type = $a["type"] ?? "singlechoice"; // if the type is missing, then it's a single choice challenge
- switch ($type) {
- case "singlechoice":
- return new SingleChoiceChallenge($a);
- }
-
- return null;
- }
-
- static function constructFromCollection(array $c): array {
- $chgs = [];
- foreach ($c as $ch) {
- $chgs[] = ChallengeFactory::fromArray($ch);
- }
- return $chgs;
- }
-}
-
-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) {
- $ch->randomize();
- }
- }
-
- // -------------
-
- function getMaxSumMark(): float
- {
- $msm = 0.0;
- foreach ($this->challenges as &$ch) {
- $msm += $ch->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->challenges = ChallengeFactory::constructFromCollection($a["challenges"]);
- 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->challenges = ChallengeFactory::constructFromCollection($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($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 = []): array
- {
- $chgs = [];
- foreach ($this->challenges as $ch) {
- $chgs[] = $ch->toArray();
- }
-
- $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" => $chgs,
- "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);
- }
-
- function isChallengeIdInsideBounds(int $chidx): bool {
- return ($chidx >= 0) && ($chidx < $this->getChallengeCount());
- }
-
- // Save answer. Asserting $safe prevents saving answers to a concluded test.
- function saveAnswer(int $chidx, string $ans, bool $safe = true): bool
- {
- if (!$safe || $this->state === self::TEST_ONGOING) {
- if ($this->isChallengeIdInsideBounds($chidx)) {
- $this->challenges[$chidx]->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->isChallengeIdInsideBounds($chidx)) {
- $this->challenges[$chidx]->clearAnswer();
- $this->commitMods();
- return true;
- }
- }
- return false;
- }
-
- // Conclude test.
- function concludeTest(): void
- {
- // summarize points
- $mark_sum = 0.0;
- foreach ($this->challenges as &$ch) {
- $mark_sum += $ch->getMark();
- }
-
- // set state and fill summary
- $this->state = 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;
- }
-}
+require_once "Test.php";
class TestMgr
{
@@ -598,7 +71,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_ONGOING]]);
+ $ongoing_tests = $this->db->findBy([$fetch_criteria, "AND", ["state", "=", Test::TEST_ONGOING]]);
if (count($ongoing_tests) !== 0) { // if there's an ongoing test
$testid = $ongoing_tests[0]["_id"];
$test = $this->getTest($testid);
@@ -643,7 +116,7 @@ class TestMgr
}
// Get test results by game ID.
- function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_challenge_data, bool $best_ones_only, array ...$furtherFilters): array
+ function getResultsByGameId(string $gameid, string $filter, string $orderby, bool $exclude_task_data, bool $best_ones_only, array ...$furtherFilters): array
{
$qb = $this->db->createQueryBuilder();
$qb = $qb->where(["gameid", "=", (int)$gameid]);
@@ -679,8 +152,8 @@ class TestMgr
$qb->orderBy($ordering);
}
- // excluding challenge data
- if ($exclude_challenge_data) {
+ // excluding task data
+ if ($exclude_task_data) {
$qb->except(["challenges"]);
}
@@ -731,42 +204,42 @@ class TestMgr
$qb->select(["challenges"]);
$entries = $qb->getQuery()->fetch();
- $challenge_indices = [];
+ $task_indices = [];
// count answers
$aggregated = [];
foreach ($entries as $entry) {
- foreach ($entry["challenges"] as $challenge) {
- $correct_answer = $challenge["answers"][$challenge["correct_answer"]];
- $compound = $challenge["question"] . $correct_answer . count($challenge["answers"]) . $challenge["image_url"];
+ foreach ($entry["challenges"] as $task) {
+ $correct_answer = $task["answers"][$task["correct_answer"]];
+ $compound = $task["question"] . $correct_answer . count($task["answers"]) . $task["image_url"];
$idhash = md5($compound);
- // if this is a new challenge to the list...
- if (!isset($challenge_indices[$idhash])) {
- $challenge_indices[$idhash] = count($challenge_indices);
- $challenge_info = [ // copy challenge info
+ // if this is a new task to the list...
+ if (!isset($task_indices[$idhash])) {
+ $task_indices[$idhash] = count($task_indices);
+ $task_info = [ // copy challenge info
"hash" => $idhash,
- "image_url" => $challenge["image_url"],
- "question" => $challenge["question"],
- "answers" => $challenge["answers"],
+ "image_url" => $task["image_url"],
+ "question" => $task["question"],
+ "answers" => $task["answers"],
"correct_answer" => $correct_answer,
- "player_answers" => array_fill(0, count($challenge["answers"]), 0),
- "answer_count" => count($challenge["answers"]),
+ "player_answers" => array_fill(0, count($task["answers"]), 0),
+ "answer_count" => count($task["answers"]),
"skipped" => 0
];
- $aggregated[$challenge_indices[$idhash]] = $challenge_info; // insert challenge info
+ $aggregated[$task_indices[$idhash]] = $task_info; // insert task info
}
- // fetch challenge index
- $challenge_idx = $challenge_indices[$idhash];
+ // fetch task index
+ $task_idx = $task_indices[$idhash];
// add up player answer
- $player_answer = trim($challenge["player_answer"]);
+ $player_answer = trim($task["player_answer"]);
if (($player_answer !== "") && ($player_answer != -1)) { // player answered
- $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]++;
+ $answer_idx = array_search($task["answers"][$task["player_answer"]], $aggregated[$task_idx]["answers"]); // transform player answer index to report answer index
+ $aggregated[$task_idx]["player_answers"][(int)$answer_idx]++;
} else { // player has not answered or provided an unprocessable answer
- $aggregated[$challenge_idx]["skipped"]++;
+ $aggregated[$task_idx]["skipped"]++;
}
}
}
@@ -786,7 +259,7 @@ class TestMgr
}
}
- // match challenges
+ // match tasks
return $aggregated;
}
@@ -811,7 +284,7 @@ class TestMgr
{
$query = [["time_limited", "=", true], "AND", ["end_limit_time", "<", time()]];
if ($ongoingOnly) {
- $query = [...$query, "AND", ["state", "=", TEST_ONGOING]];
+ $query = [...$query, "AND", ["state", "=", Test::TEST_ONGOING]];
}
$qb = $this->db->createQueryBuilder();
diff --git a/class/TestSummary.php b/class/TestSummary.php
new file mode 100644
index 0000000..94693df
--- /dev/null
+++ b/class/TestSummary.php
@@ -0,0 +1,65 @@
+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];
+ }
+}
\ No newline at end of file
diff --git a/class/User.php b/class/User.php
new file mode 100644
index 0000000..3f02dba
--- /dev/null
+++ b/class/User.php
@@ -0,0 +1,134 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/class/UserMgr.php b/class/UserMgr.php
index 725dc42..1a1f0dc 100644
--- a/class/UserMgr.php
+++ b/class/UserMgr.php
@@ -8,136 +8,7 @@ require_once "AutoStoring.php";
require_once "privilege_levels.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;
- }
-}
+require_once "User.php";
class UserMgr
{
diff --git a/class/Utils.php b/class/Utils.php
new file mode 100644
index 0000000..2abc6f5
--- /dev/null
+++ b/class/Utils.php
@@ -0,0 +1,35 @@
+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;
}
}
diff --git a/common_func.php b/common_func.php
index d2cb088..470e800 100644
--- a/common_func.php
+++ b/common_func.php
@@ -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(",", str_replace(" ", "", $str)));
+ return array_map(fn($a) => trim($a), explode(",", trim($str)));
} else {
return [];
}
diff --git a/composer.json b/composer.json
index 0bd6d47..a7fb618 100644
--- a/composer.json
+++ b/composer.json
@@ -4,6 +4,9 @@
"ext-http": "*",
"ext-mbstring" : "*",
"ext-zip": "*",
- "ext-fileinfo": "*"
+ "ext-fileinfo": "*",
+ "ext-luasandbox": "*",
+ "phpoffice/phpspreadsheet": "^5.1",
+ "symfony/expression-language": "^7.3"
}
-}
\ No newline at end of file
+}
diff --git a/game_manager_frame.php b/game_manager_frame.php
index 3cd948d..4d03230 100644
--- a/game_manager_frame.php
+++ b/game_manager_frame.php
@@ -71,10 +71,10 @@ if (!get_autologin_state() || (($user_data["privilege"] !== PRIVILEGE_CREATOR) &
${this.correctAnswer}(${this.dst_base})`;
+ }
+
+ fromArray(a) {
+ const regex = /([0-9]+)([suc]):([0-9]+)->([0-9]+)([suc]):([0-9]+)/g;
+ let parts = [...a["instruction"].matchAll(regex)][0];
+
+ this.src_base = parts[1];
+ this.dst_base = parts[4];
+ this.src_len = parts[6];
+
+ let src_exp = `${a["source"]}(${parts[1]}) =`;
+ let dst_exp = `(${parts[4]}) (${parts[6]} digiten)`;
+
+ super.fromArray(a);
+
+ this.src_sec.innerHTML = src_exp;
+ this.dst_sec.innerHTML = dst_exp;
+
+ this.updateAnswerFieldLength();
+ }
+
+ updateAnswerFieldLength() {
+ this.answer_tf.style.width = this.answer_tf.value.length + "ch";
+ }
+}
+
+class Switch extends HTMLElement {
+ constructor() {
+ super();
+
+ this.state = "-";
+ this.highlight = "-";
+ this.M = 2;
+ this.disabled = false;
+
+ this.shadow = this.attachShadow({mode: "open"});
+ this.createStyle();
+ this.createElements();
+ }
+
+ createStyle() {
+ this.css = document.createElement("style");
+ this.css.innerHTML = `
+ section.frame {
+ display: inline-block;
+ border: 1pt solid black;
+ }
+ section.button {
+ display: inline-block;
+ padding: 0.2em 0.5em;
+ cursor: pointer;
+ font-family: 'Source Code Pro', monospace;
+ }
+ section.button[disabled="false"]:hover {
+ background-color: #408d8d;
+ color: white;
+ }
+ section.button[highlight="true"] {
+ background-color: #aa8a7d;
+ color: white;
+ }
+ section.button[selected="true"] {
+ background-color: #176767;
+ color: white;
+ }
+ section.button:not(:last-child) {
+ border-right: 1pt dotted black;
+ }
+ `;
+ this.shadow.append(this.css);
+ }
+
+ createElements() {
+ let frame = document.createElement("section");
+ frame.classList.add("frame");
+ let btns = [];
+ for (let i = 0; i < this.M; i++) {
+ let btn = document.createElement("section");
+ btn.classList.add("button");
+ btn.innerText = i.toString();
+ btn.id = "btn_" + i.toString();
+ btn.setAttribute("disabled", "false");
+ btns.push(btn);
+
+ btn.addEventListener("click", (e) => {
+ if (!this.disabled) {
+ this.setState(i);
+ this.dispatchEvent(new Event("change"));
+ }
+ })
+
+ frame.append(btn);
+ }
+
+ document.createElement("section");
+
+ this.frame = frame;
+ this.btns = btns;
+
+ this.shadow.append(frame);
+
+ this.setDisabled(false);
+ }
+
+ setState(state) {
+ this.state = state;
+ for (let i = 0; i < this.M; i++) {
+ this.btns[i].setAttribute("selected", (i.toString() === this.state.toString()) ? "true" : "false");
+ }
+ }
+
+ setHighlight(hl) {
+ this.highlight = hl;
+ for (let i = 0; i < this.M; i++) {
+ this.btns[i].setAttribute("highlight", (i.toString() === this.highlight.toString()) ? "true" : "false");
+ }
+ }
+
+ getState() {
+ return this.state;
+ }
+
+ setDisabled(disabled) {
+ this.disabled = disabled;
+ this.frame.setAttribute("disabled", disabled ? "true" : "false");
+ }
+}
+
+var truth_table_css = `
+ table#tt {
+ border: 1.5pt solid #176767;
+ margin: 0.5em auto;
+ border-spacing: 0;
+ font-family: 'Source Code Pro', monospace;
+ }
+ table#tt tr:not(:last-child) td {
+ border-bottom: 1.2pt solid black;
+ }
+ table#tt th {
+ border-bottom: 1.5pt dotted black
+ }
+ table#tt td, table#tt th {
+ min-width: 3ch;
+ text-align: center;
+ }
+ table#tt td:last-child, table#tt th:last-child {
+ border-left: 1.5pt dashed black;
+ }
+`;
+
+class TruthTableTask extends PicturedTask {
+ constructor() {
+ super("truthtable");
+
+ this.input_variables = [];
+ this.output_variable = "";
+ this.output_switches = [];
+ }
+
+ createStyle() {
+ super.createStyle();
+
+ this.css.innerHTML += truth_table_css;
+ }
+
+ createElements() {
+ super.createElements();
+
+ let tt = document.createElement("table");
+ tt.id = "tt";
+
+ this.answer_container.append(tt);
+
+ this.tt = tt;
+ }
+
+ updatePlayerAnswer() {
+ let pa = "";
+ for (let i = 0; i < this.output_switches.length; i++) {
+ pa += this.output_switches[i].getState();
+ }
+ super.playerAnswer = pa;
+ }
+
+ buildTTable() {
+ let N = this.input_variables.length;
+ let M = (1 << N);
+
+ let inside = "| " + this.input_variables[i] + " | "; + } + table += "" + this.output_variable + " | "; + table += "
|---|---|
| " + ((i >> (N - j - 1)) & 1).toString() + " | "; + } + table += "" + this.truth_table.charAt(i) + " | " + table += "
" + correct_answer + "" ];
+ }
+
+ get correctAnswer() {
+ return super.correctAnswer;
+ }
+}
+
+class VerilogTask extends PicturedTask {
+ //static observedAttributes = ["language"]
+ constructor() {
+ super("verilog");
+ }
+
+ createStyle() {
+ super.createStyle();
+
+ this.css.innerHTML += `
+ section.editor-sec {
+ margin-top: 1em;
+ min-height: 24em;
+ font-family: 'JetBrains Mono', monospace;
+ }
+ div.ace_content {
+ font-variant-ligatures: none;
+ }
+ section#explain-sec {
+ font-family: 'JetBrains Mono', monospace;
+ margin-top: 0.5em;
+ width: calc(100% - 0.4em);
+ padding: 0.2em;
+ background: #e5e5e57f;
+ }
+ section#correct-answer-title {
+ padding: 1em 0 0.5em 0.2em;
+ font-weight: bold;
+ }
+ `;
+ }
+
+ createElements() {
+ super.createElements();
+
+ let editor_sec = document.createElement("section");
+ editor_sec.classList.add("editor-sec");
+ this.answer_container.append(editor_sec);
+
+ let editor = ace.edit(editor_sec, {
+ theme: "ace/theme/chrome",
+ mode: "ace/mode/verilog"
+ });
+ editor.renderer.attachToShadowRoot();
+
+ editor.addEventListener("blur", () => {
+ this.uploadAnswer();
+ });
+
+ let explain_sec = document.createElement("section");
+ explain_sec.id = "explain-sec";
+ this.ca_section.append(explain_sec);
+
+ let solution_title_sec = document.createElement("section");
+ solution_title_sec.innerText = "Egy lehetséges megoldás:";
+ solution_title_sec.id = "correct-answer-title";
+
+ this.ca_section.append(solution_title_sec);
+
+ let solution_sec = document.createElement("section");
+ solution_sec.id = "solution-sec";
+ solution_sec.classList.add("editor-sec");
+ let solution_editor = ace.edit(solution_sec, {
+ theme: "ace/theme/chrome",
+ mode: "ace/mode/verilog",
+ });
+ solution_editor.setReadOnly(true);
+ solution_editor.renderer.attachToShadowRoot();
+
+ this.ca_section.append(solution_sec);
+
+ this.editor_sec = editor_sec;
+ this.editor = editor;
+ this.explain_sec = explain_sec;
+ this.solution_title_sec = solution_title_sec;
+ this.solution_sec = solution_sec;
+ this.solution_editor = solution_editor;
+ }
+
+ set playerAnswer(player_answer) {
+ this.editor.setValue(player_answer);
+ this.editor.clearSelection();
+ }
+
+ get playerAnswer() {
+ return this.editor.getValue();
+ }
+
+ set correctAnswer(player_answer) {
+ this.solution_editor.setValue(player_answer);
+ this.solution_editor.clearSelection();
+ }
+
+ get correctAnswer() {
+ return super.correctAnswer;
+ }
+
+ fromArray(a) {
+ super.fromArray(a);
+
+ this.explain_sec.innerText = a["compile_log"];
+ }
+
+ updateEditorFreezeState() {
+ this.editor.setReadOnly(this.isConcluded || this.isViewOnly);
+ }
+
+ set isConcluded(concluded) {
+ super.isConcluded = concluded;
+
+ this.updateEditorFreezeState();
+
+ if (concluded) {
+ this.solution_editor.setValue(this.correctAnswer);
+ }
+ }
+
+ get isConcluded() {
+ return super.isConcluded;
+ }
+
+ set isViewOnly(viewOnly) {
+ super.isViewOnly = viewOnly;
+
+ this.updateEditorFreezeState();
+ }
+
+ get isViewOnly() {
+ return super.isViewOnly;
+ }
+
+ // attributeChangedCallback(name, oldVal, newVal) {
+ // switch (name) {
+ // case "language":
+ // editor.session.setMode("ace/mode/" + newVal.toLowerCase());
+ // break;
+ // }
+ // }
+
+
+}
+
+customElements.define('singlechoice-task', SingleChoiceTask);
+customElements.define('openended-task', OpenEndedTask);
+customElements.define('numberconversion-task', NumberConversionTask);
+customElements.define('slide-switch', Switch);
+customElements.define('truthtable-task', TruthTableTask);
+customElements.define('logicfunction-task', LogicFunctionTask);
+customElements.define('verilog-task', VerilogTask);
+
+
diff --git a/js/terminal.js b/js/terminal.js
new file mode 100644
index 0000000..dcb5f9f
--- /dev/null
+++ b/js/terminal.js
@@ -0,0 +1,17 @@
+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;
+ });
+ }
+}
\ No newline at end of file
diff --git a/js/testground.js b/js/testground.js
index dd483f6..a3b0eca 100644
--- a/js/testground.js
+++ b/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 challenge_n = summary["challenge_n"];
- let r = Math.ceil((correct_answer_n / challenge_n) * 100);
- percentageS.innerHTML = `${r}% (${correct_answer_n}/${challenge_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 start_time = unix_time_to_human_readable(test_data["start_time"]);
let end_time = unix_time_to_human_readable(test_data["end_time"]);
@@ -70,87 +70,27 @@ function populate_infobox(test_data, view_only) {
}
}
-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) {
+function populate_tasks(tasks, concluded, view_only = false, gameid) {
let test_display = document.getElementById("test_display");
test_display.innerHTML = "";
- 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);
+ Task.sequence_number = 0;
- 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);
+ 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"];
}
-
- 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);
+ task_element.isConcluded = concluded;
+ task_element.isViewOnly = view_only;
+ task_element.fromArray(task);
+ test_display.appendChild(task_element);
});
-
- mark_answers(challenges, view_only);
-
- MathJax.typeset();
}
function populate_all(test_id, gameid, view_only) {
@@ -162,22 +102,33 @@ function populate_all(test_id, gameid, view_only) {
request(req).then(resp => {
TEST_DATA = JSON.parse(resp);
let concluded = TEST_DATA["state"] === "concluded";
- populate_challenges(TEST_DATA["challenges"], concluded, view_only, gameid);
+ populate_tasks(TEST_DATA["challenges"], concluded, view_only, gameid);
populate_infobox(TEST_DATA, view_only);
});
}
-function save_answer(chidx, aidx) {
+function save_answer(tidx, ans) {
let req = {
action: "save_answer",
testid: TEST_DATA["_id"],
- challenge_index: chidx,
- answer_index: aidx,
+ task_index: tidx,
+ answer: ans,
};
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"]
@@ -185,4 +136,91 @@ function submit_test() {
request(req).then(resp => {
populate_all(TEST_DATA["_id"], TEST_DATA["gameid"], false);
});
-}
\ No newline at end of file
+}
+
+// ---------
+
+// 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;
+// }
+// }
+// }
diff --git a/js/usermgr.js b/js/usermgr.js
index f78ded3..ab9c3e7 100644
--- a/js/usermgr.js
+++ b/js/usermgr.js
@@ -83,7 +83,7 @@ function create_edit_user(user = null) {
nicknameF.value = user["nickname"];
nicknameF.readOnly = true;
realnameF.value = user["realname"];
- passwordF.type = "password";
+ passwordF.task_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.type = "text";
+ passwordF.task_type = "text";
passwordF.value = generateRandomString();
passwordF.readOnly = true;
groupsF.value = "";
diff --git a/main.php b/main.php
index c99888e..5860fb9 100644
--- a/main.php
+++ b/main.php
@@ -29,8 +29,15 @@ $privilege = $user_data["privilege"];
+
+
+
+
+
+
+
-