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 for ($i = 1; $i < $n; $i++) { $row = &$table[$i]; // fetch row // 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" => Game::explodeFlags($row[0] ?? ""), "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": $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"] !== "") { $a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]); $a["image_type"] = "url"; } // generate the task $this->tasks[] = TaskFactory::fromArray($a, $this); } $result["n"] = $n - 1; 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; } }