diff --git a/class/Game.php b/class/Game.php index 2ae5475..2ecd6b1 100644 --- a/class/Game.php +++ b/class/Game.php @@ -4,6 +4,8 @@ require_once "vendor/autoload.php"; require_once "AutoStoring.php"; +require_once "Utils.php"; + class Game extends AutoStoring { public const DEFAULT_GAME_PROPERTIES = [ @@ -161,7 +163,15 @@ class Game extends AutoStoring // 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(); + 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. @@ -225,7 +235,7 @@ class Game extends AutoStoring // construct task record $a = [ "question" => trim($csvline[0]), - "image_url" => trim($csvline[1]), + "image_data" => trim($csvline[1]), "correct_answer" => 0, "answers" => array_filter(array_slice($csvline, 2), function ($v) { return trim($v) !== ""; @@ -233,8 +243,9 @@ class Game extends AutoStoring ]; // if an image is attached to the task, then give a random name to the image - if ($a["image_url"] !== "") { - $a["image_url"] = $this->obfuscateAttachedImage($a["image_url"]); + if ($a["image_data"] !== "") { + $a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]); + $a["image_type"] = "url"; } // store the task @@ -285,7 +296,7 @@ class Game extends AutoStoring $row = &$table[$i]; // fetch row $a = [ // create initializing array "question" => trim($row[0]), - "image_url" => trim($row[1]), + "image_data" => trim($row[1]), "correct_answer" => 0, "answers" => array_filter(array_slice($row, 2), function ($v) { return trim($v ?? "") !== ""; @@ -293,8 +304,9 @@ class Game extends AutoStoring ]; // obfuscate image filename - if ($a["image_url"] !== "") { - $a["image_url"] = $this->obfuscateAttachedImage($a["image_url"]); + if ($a["image_data"] !== "") { + $a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]); + $a["image_type"] = "url"; } // create the task @@ -327,7 +339,8 @@ class Game extends AutoStoring return -1; } - private static function explodeFlags(string $fs): array { + private static function explodeFlags(string $fs): array + { $flags = explode(",", trim($fs)); return array_filter($flags, fn($v) => trim($v) !== ""); } @@ -371,9 +384,9 @@ class Game extends AutoStoring $a = [ "flags" => Game::explodeFlags($row[0] ?? ""), "type" => strtolower($select_fn(["Típus", "Type"])), - "generator" => $select_fn(["Generátor", "Generator"]), - "image_url" => $select_fn(["Kép", "Image"]), + "image_data" => $select_fn(["Kép", "Image"]), "question" => $select_fn(["Kérdés", "Question"]), + "lua_script" => $select_fn(["Lua"]), ]; // convert into @@ -383,15 +396,35 @@ class Game extends AutoStoring $a["correct_answer"] = 0; break; case "openended": - $a["correct_answers"] = $extract_unlabeled_fn(); + $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->tasks[] = TaskFactory::fromArray($a, $this); } $result["n"] = $n - 1; diff --git a/class/LogicFunction.php b/class/LogicFunction.php index c677f1a..fbf912c 100644 --- a/class/LogicFunction.php +++ b/class/LogicFunction.php @@ -2,93 +2,95 @@ use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -class LogicFunction -{ - public array $input_vars; - public string $verilog_form; - public string $tex_form; +require_once "PythonUtils.php"; - public function __construct(array $input_vars = [], string $verilog_form = "", string $tex_form = "") +class LogicFunction implements JsonSerializable +{ + private static ExpressionLanguage $EXP_LANG; // expression language for linting an evaluation + private array $input_vars; // array of input variables + private string $expression; // the logic function in the bitwise Verilog-form + + public static function convertToVerilogBitwiseForm(string $expression): string { - $this->input_vars = $input_vars; - $this->verilog_form = $verilog_form; - $this->tex_form = $tex_form; + return str_replace(["/", "!", "*", "+"], ["~", "~", "&", "|"], $expression); } - public function getTruthTable(): array { - $tt = []; + public static function collectVariables(string $expression): array + { + preg_match_all("/\w/", $expression, $variables); + return array_filter(array_unique($variables[0]), fn($v) => !empty($v) && !is_numeric($v[0])); + } + public function __construct(string $expression = "", array $input_vars = []) + { + $this->setExpression($expression); + $this->setInputVars(($input_vars === []) ? self::collectVariables($this->expression) : $input_vars); + } + + public function getTruthTable(): string + { $N = count($this->input_vars); - $M = pow(2, $N); + if ($N == 0) { + return ""; + } - $exp_lang = new ExpressionLanguage(); + $M = pow(2, $N); $vars = []; foreach ($this->input_vars as $var) { $vars[$var] = 0; } - $cooked_form = str_replace(["&", "|", "~"], ["&&", "||", "!"], $this->verilog_form); - printf("Cooked: %s\n", $cooked_form); + $expression = $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]]); +// printf("%d ", $vars[$this->input_vars[$k]]); } - $out = $exp_lang->evaluate($cooked_form, $vars); - printf("%d\n", $out); + $out = self::$EXP_LANG->evaluate($expression, $vars); +// printf("%d\n", $out); $tt[] = $out; } - return $tt; + return join("", array_map(fn($r) => ($r === True) ? 1 : 0, $tt)); } public static function genRandom(array $input_vars, int $min_depth = 2, int $max_depth = 3): LogicFunction { - function genTerm(array $vars, int $ftn, int $tn, int $mind, int $maxd, bool $top = true, int $opindex = 1): array + function genTerm(array $vars, int $ftn, int $tn, int $mind, int $maxd, bool $top = true, int $opindex = 1): string { - $verilog_term = ""; - $tex_term = ""; + $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)]; - $verilog_term = ($neg ? "~" : "") . $var; - $tex_term = $neg ? ("\\overline{" . $var . "}") : $var; + $term = ($neg ? "~" : "") . $var; } else { $depth = random_int(0, max(0, $maxd - 1)); $verilog_ops = [" & ", " | "]; - $tex_ops = ["", " | "]; - $verilog_op = $verilog_ops[$opindex]; - $tex_op = $tex_ops[$opindex]; - - $verilog_term = !$top ? "(" : ""; - $tex_term = !$top ? "\\left(" : ""; + $term = !$top ? "(" : ""; $nextopindex = ($opindex === 0) ? 1 : 0; for ($i = 0; $i < $m; $i++) { - $term = genTerm($vars, (($mind - 1) > 0) ? $ftn : 0, $tn, $mind - 1, $depth, false, $nextopindex); - $verilog_term .= $term["verilog"]; - $tex_term .= $term["tex"]; + $subterm = genTerm($vars, (($mind - 1) > 0) ? $ftn : 0, $tn, $mind - 1, $depth, false, $nextopindex); + $term .= $subterm; if ($i < $m - 1) { - $verilog_term .= $verilog_op; - $tex_term .= $tex_op; + $term .= $verilog_op; } } - $verilog_term .= !$top ? ")" : ""; - $tex_term .= !$top ? "\\right)" : ""; + $term .= !$top ? ")" : ""; } - return ["verilog" => $verilog_term, "tex" => $tex_term]; + return $term; } $term = genTerm($input_vars, count($input_vars), count($input_vars), $min_depth, $max_depth); - - return new LogicFunction($input_vars, $term["verilog"], $term["tex"]); - + return new LogicFunction($term, $input_vars); } public static function genRandomDF($input_vars): LogicFunction @@ -97,57 +99,157 @@ class LogicFunction $states = pow(2, $N); $verilog_term = ""; - $tex_term = ""; for ($i = 0; $i < $states; $i++) { - - $verilog_inside = ""; - $tex_inside = ""; - + $inside = ""; $omit = random_int(0, 1); // omit the variable 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) { - $verilog_inside .= "~" . $term; - $tex_inside .= "\\overline{" . $term . "}"; + $inside .= "~" . $term; } else { - $verilog_inside .= $term; - $tex_inside .= $term; + $inside .= $term; } - if ($j < ($N - 1)) { - $verilog_inside .= " & "; - $tex_inside .= ""; + $inside .= " & "; } } } - //$verilog_inside = rtrim($verilog_inside, "&"); - //$tex_inside = rtrim($tex_inside, "\\&"); - - if ($verilog_inside !== "") { + if ($inside !== "") { $verilog_term .= "("; - $tex_term .= "\\left("; - - $verilog_term .= $verilog_inside; - $tex_term .= $tex_inside; - + $verilog_term .= $inside; $verilog_term .= ")"; - $tex_term .= "\\right)"; - if (($i < ($states - 1)) && !$omit) { $verilog_term .= " | "; - $tex_term .= " | "; } } } $verilog_term = rtrim($verilog_term, "| "); - $tex_term = rtrim($tex_term, "| "); - - - return new LogicFunction($input_vars, $verilog_term, $tex_term); + return new LogicFunction($verilog_term, $input_vars); } -} \ No newline at end of file + + 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($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 + $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; + } + } + return join(" | ", $minterms); + } + + public function drawNetwork(string $fn, string $outvar = "f"): void { + PythonUtils::execPy("draw_logic_network.py", [ $this->getExpression(), $outvar, $fn ]); + } +} + +LogicFunction::initStatic(); \ No newline at end of file diff --git a/class/LogicUtils.php b/class/LogicUtils.php new file mode 100644 index 0000000..48a937c --- /dev/null +++ b/class/LogicUtils.php @@ -0,0 +1,54 @@ += ($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, $rep); + $numa_str = self::extend($numa_str, $base, $digits, true); + } + } else { + $numa_str = self::extend($numa_str, $base, $digits, false); + } + return $numa_str; + } +} \ No newline at end of file diff --git a/class/PythonUtils.php b/class/PythonUtils.php new file mode 100644 index 0000000..f2ffa17 --- /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"; + $ret = shell_exec($python_cmd); // execute python script + } +} \ No newline at end of file diff --git a/class/Task.php b/class/Task.php index 5b3af0b..a2b514e 100644 --- a/class/Task.php +++ b/class/Task.php @@ -2,23 +2,69 @@ class Task implements JsonSerializable { - protected string $type; // task type - protected string $question; // the task title + private string $type; // task type + private string $question; // the task title protected mixed $player_answer; // answer given by the player protected mixed $correct_answer; - protected float $max_mark; // maximum points that can be collected at this task - protected bool $is_template; // this task is a template - protected array $flags; // task flags + private float $max_mark; // maximum points that can be collected at this task + private float $mark; // earned points + private bool $is_template; // this task is a template + private array $flags; // task flags + private string $lua_script; // path to the corresponding Lua script + private Game|Test|null $governor; // object that governs this task + private LuaSandbox|null $lua_sandbox; // Lua sandbox, initially NULL + + // ------------- + + protected function addLuaLibraries(): void { + // register member methods + $method_names = get_class_methods($this); + $methods = []; + foreach ($method_names as $method_name) { + $methods[$method_name] = fn() => [ call_user_func(array(&$this, $method_name), ...func_get_args()) ]; + } + $this->lua_sandbox->registerLibrary("task", $methods); + + // register generic functionality + $this->lua_sandbox->registerLibrary("php", [ + "print" => function($str) { + printf("%s\n", $str); + }, + "replace" => "str_replace", + "replace_field" => function($field, $replacement, $str) { + return [ str_replace("{{" . $field . "}}", $replacement, $str) ]; + } + ]); + } + 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->lua_script = $a["lua_script"] ?? ""; + + $this->governor = null; + $this->lua_sandbox = null; } function setQuestion(string $question): void @@ -65,22 +111,48 @@ class Task implements JsonSerializable return $this->max_mark; } - function getMark(): float - { - return 1.0; + function setMark(float $mark): void { + $this->mark = $mark; } - function toArray(): array + function getMark(): float + { + return $this->mark; + } + + private function luaCheck(): void { + //$lua = new Lua($this->getGameDir() . DIRECTORY_SEPARATOR . $this->lua_script); + return; + } + + protected function staticCheck(): void { + return; + } + + 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, - "is_template" => $this->is_template, - "flags" => $this->flags, + "mark" => $this->mark, "correct_answer" => $this->correct_answer, ]; + if ($mode === "all") { + $a["is_template"] = $this->is_template; + $a["flags"] = $this->flags; + $a["lua_script"] = $this->lua_script; + } + if (!$this->isTemplate()) { $a["player_answer"] = $this->player_answer; } @@ -118,6 +190,16 @@ class Task implements JsonSerializable 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; + } + function getPlayerAnswer(): mixed { return $this->player_answer; } @@ -137,8 +219,32 @@ class Task implements JsonSerializable return $this->correct_answer; } + private function luaRandomize(): void { + $this->luaCall("randomize"); + } + function randomize(): void { + if ($this->lua_script !== "") { + $this->luaRandomize(); + } return; } + + 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 index d88ccc1..aca9126 100644 --- a/class/TaskFactory.php +++ b/class/TaskFactory.php @@ -6,30 +6,44 @@ require_once "Tasks/OpenEndedTask.php"; require_once "Tasks/NumberConversionTask.php"; +require_once "Tasks/TruthTableTask.php"; + +require_once "Tasks/VerilogTask.php"; + class TaskFactory { - static function fromArray(array $a): Task|null + static function fromArray(array $a, Game|Test|null &$governor = null): Task|null { $type = $a["type"] ?? "singlechoice"; // if the type is missing, then it's a single choice task switch ($type) { case "singlechoice": - return new SingleChoiceTask($a); + $task = new SingleChoiceTask($a); break; case "openended": - return new OpenEndedTask($a); + $task = new OpenEndedTask($a); break; case "numberconversion": - return new NumberConversionTask($a); + $task = new NumberConversionTask($a); break; + case "truthtable": + $task = new TruthTableTask($a); + break; + case "verilog": + $task = new VerilogTask($a); + break; + default: + return null; } - return null; + $task->setGovernor($governor); + return $task; } - static function constructFromCollection(array $c): array { + static function constructFromCollection(array $c, Game|Test|null &$governor = null): array + { $chgs = []; foreach ($c as $ch) { - $chgs[] = TaskFactory::fromArray($ch); + $chgs[] = TaskFactory::fromArray($ch, $governor); } return $chgs; } diff --git a/class/Tasks/NumberConversionTask.php b/class/Tasks/NumberConversionTask.php index 7bd8c60..21bc9e0 100644 --- a/class/Tasks/NumberConversionTask.php +++ b/class/Tasks/NumberConversionTask.php @@ -2,6 +2,8 @@ require_once "OpenEndedTask.php"; +require_once "class/LogicUtils.php"; + class NumberConversionTask extends OpenEndedTask { protected string $instruction; // instruction word @@ -52,9 +54,9 @@ class NumberConversionTask extends OpenEndedTask $this->correct_answer = $a["correct_answer"] ?? "---"; } - public function toArray(): array + public function toArray(string $mode = "all"): array { - $a = parent::toArray(); + $a = parent::toArray($mode); $a["instruction"] = $this->instruction; $a["source"] = $this->source; @@ -63,58 +65,10 @@ class NumberConversionTask extends OpenEndedTask return $a; } - private static function extend(string $num, int $base, int $exnd, bool $comp): string - { - $fd = (int)(base_convert($num[0], $base, 10)); // get first digit as a number - $extd = (string)(($comp && ($fd >= ($base / 2))) ? ($base - 1) : 0); // get the extension digit - return str_pad((string)($num), $extd, $extd, STR_PAD_LEFT); // extend to the left - } - - private 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); - } - - private 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, $rep); - $numa_str = self::extend($numa_str, $base, $digits, true); - } - } else { - $numa_str = self::extend($numa_str, $base, $digits, false); - } - return $numa_str; - } - 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 @@ -144,16 +98,18 @@ class NumberConversionTask extends OpenEndedTask $m = random_int($min, $max); // create the question and the answer - $this->correct_answer = self::changeRepresentation($m, $this->dst_base, $this->dst_rep, $this->dst_n_digits); - $this->source = self::changeRepresentation($m, $this->src_base, $this->src_rep, $this->src_n_digits); + $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 getMark(): float + public function staticCheck(): void { + $mark = 0.0; if ($this->hasFlag("acceptwithoutleadingzeros")) { - return (ltrim($this->player_answer, " 0") === ltrim($this->correct_answer, "0")) ? 1.0 : 0.0; + $mark = (ltrim($this->player_answer, " 0") === ltrim($this->correct_answer, "0")) ? 1.0 : 0.0; } else { - return (trim($this->player_answer) === trim($this->correct_answer)) ? 1.0 : 0.0; + $mark = (trim($this->player_answer) === trim($this->correct_answer)) ? 1.0 : 0.0; } + $this->setMark($mark); } } \ No newline at end of file diff --git a/class/Tasks/OpenEndedTask.php b/class/Tasks/OpenEndedTask.php index 50a353d..ff83c8e 100644 --- a/class/Tasks/OpenEndedTask.php +++ b/class/Tasks/OpenEndedTask.php @@ -8,7 +8,8 @@ class OpenEndedTask extends PicturedTask { parent::__construct("openended", $a); - $this->correct_answer = $a["correct_answer"] ?? null; + $this->correct_answer = $a["correct_answer"] ?? []; + $this->player_answer = $this->player_answer ?? ""; $this->setMaxMark(1.0); } @@ -20,7 +21,7 @@ class OpenEndedTask extends PicturedTask { // collect transformations $transform_fns = []; - foreach ($this->flags as $flag) { + foreach ($this->getFlags() as $flag) { switch ($flag) { case "makeuppercase": $transform_fns[] = "strtoupper"; @@ -45,18 +46,14 @@ class OpenEndedTask extends PicturedTask return true; } - public function clearAnswer(): void + public function staticCheck(): void { - $this->player_answer = ""; + $mark = in_array($this->player_answer, $this->correct_answer) ? 1.0 : 0.0; + $this->setMark($mark); } - public function getMark(): float - { - return in_array($this->player_answer, $this->correct_answer) ? 1.0 : 0.0; - } - - function toArray(): array { - $a = parent::toArray(); + function toArray(string $mode = "all"): array { + $a = parent::toArray($mode); $a["correct_answer"] = $this->correct_answer; return $a; } diff --git a/class/Tasks/PicturedTask.php b/class/Tasks/PicturedTask.php index 721d18a..ddab455 100644 --- a/class/Tasks/PicturedTask.php +++ b/class/Tasks/PicturedTask.php @@ -4,28 +4,39 @@ require_once "class/Task.php"; class PicturedTask extends Task { - protected string $image_url; // the URL of the corresponding image + private string $image_data; // image data or the URL + private string $image_type; // the type of the image function __construct(string $type, array &$a = null) { parent::__construct($type, $a); - $this->image_url = $a["image_url"] ?? ""; + $this->image_data = $a["image_data"] ?? ($a["image_url"] ?? ""); + $this->image_type = $a["image_type"] ?? "none"; } - function setImageUrl(string $image_url): void + function setImageData(string $image_data): void { - $this->image_url = $image_url; + $this->image_data = $image_data; } - function getImageUrl(): string + function getImageData(): string { - return $this->image_url; + return $this->image_data; } - function toArray(): array + function setImageType(string $image_type): void { + $this->image_type = $image_type; + } + + function getImageType(): string { + return $this->image_type; + } + + function toArray(string $mode = "all"): array { - $a = parent::toArray(); - $a["image_url"] = $this->image_url; + $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 index ff0aecc..16f01c0 100644 --- a/class/Tasks/SingleChoiceTask.php +++ b/class/Tasks/SingleChoiceTask.php @@ -32,7 +32,7 @@ class SingleChoiceTask extends PicturedTask return $this->answers; } - private function isAnswerIdInsideBounds($ansid): bool + private function isAnswerIdInsideBounds(int $ansid): bool { return ($ansid >= 0) && ($ansid <= count($this->answers)); } @@ -53,20 +53,23 @@ class SingleChoiceTask extends PicturedTask $this->player_answer = -1; } - public function getMark(): float + function staticCheck(): void { - return ($this->player_answer == $this->correct_answer) ? 1.0 : 0.0; + $mark = ($this->player_answer == $this->correct_answer) ? 1.0 : 0.0; + $this->setMark($mark); } - function toArray(): array + function toArray(string $mode = "all"): array { - $a = parent::toArray(); + $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 = []; diff --git a/class/Tasks/TruthTableTask.php b/class/Tasks/TruthTableTask.php index 987449e..a65dae2 100644 --- a/class/Tasks/TruthTableTask.php +++ b/class/Tasks/TruthTableTask.php @@ -2,17 +2,71 @@ require_once "OpenEndedTask.php"; -class TruthTableTask extends OpenEndedTask +require_once "class/LogicFunction.php"; + +require_once "class/Utils.php"; + +class TruthTableTask extends PicturedTask { + private LogicFunction $lf; // logic functions + private string $output_variable; // output variable public function __construct(array $a = null) { - parent::__construct($a); + parent::__construct("truthtable", $a); - $this->setType("verilog"); + if (isset($a["function"])) { // fetching from a JSON-stored object + $this->lf = LogicFunction::fromArray($a["function"]); + } else if (isset($a["expression"], $a["input_variables"])) { // building from the scratch + $this->lf = new LogicFunction($a["expression"], $a["input_variables"]); + } else { + $this->lf = new LogicFunction(); + } + + $this->setOutputVariable($a["output_variable"] ?? "f"); } - public function randomize(): void + public function staticCheck(): void { - return parent::randomize(); // TODO: Change the autogenerated stub + $ans_tt = $this->player_answer; + $cans_tt = $this->lf->getTruthTable(); + $errs = 0; + for ($i = 0; $i < $this->lf->getNStates(); $i++) { + if (($ans_tt[$i] ?? " ") != $cans_tt[$i]) { + $errs++; + } + } + $mark = ($errs === 0) ? 1.0 : 0.0; + $this->setMark($mark); + } + + 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; + } + + public function toArray(string $mode = "all"): array + { + $a = parent::toArray($mode); + + if ($mode === "all") { + $a["function"] = $this->lf->toArray(); + } + + $a["correct_answer"] = $this->lf->getTruthTable(); + $a["input_variables"] = $this->lf->getInputVars(); + $a["output_variable"] = $this->output_variable; + + 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..a52d0f2 --- /dev/null +++ b/class/Tasks/VerilogTask.php @@ -0,0 +1,107 @@ +explanation = $a["explanation"] ?? ""; + $this->test_bench_fn = $a["test_bench_fn"] ?? ""; + } + + private function verifyCode(): bool + { + // check that no $function calls are in the code + if (str_contains($this->player_answer, "$")) { + $this->explanation .= "A kód nem tartalmazhat \$függvényhívásokat!\n"; + return false; + } + return true; + } + + private function executeTest(): bool + { + // 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 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->explanation .= "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->explanation .= substr($line, $failed_trimlen) . "\n"; + $failed_count++; + } + } + + if ($failed_count == 0) { + $this->explanation .= "Minden rendben! :)"; + } else { + $this->explanation = "$failed_count db hiba:\n\n" . $this->explanation; + } + } + + cleanup: + // remove the temporary files + @unlink($module_code_fn); + @unlink($test_bench_fn); + @unlink($output_fn); + + return $failed_count == 0; + } + + public function staticCheck(): void + { + $this->explanation = ""; + + // verify code + $mark = 0.0; + if ($this->verifyCode()) { + // run the simulation + $test_ok = $this->executeTest(); + + $mark = $test_ok ? 1.0 : 0.0; // FIXME + } + $this->setMark($mark); + } + + public function toArray(string $mode = "all"): array + { + $a = parent::toArray($mode); + + $a["explanation"] = $this->explanation; + + if ($mode == "all") { + $a["test_bench_fn"] = $this->test_bench_fn; + } + + return $a; + } +} \ No newline at end of file diff --git a/class/Test.php b/class/Test.php index be9f6a8..170cac0 100644 --- a/class/Test.php +++ b/class/Test.php @@ -32,6 +32,7 @@ class Test extends AutoStoring private function preprocessTasks(): void { foreach ($this->tasks as &$task) { + $task->setGovernor($this); // set the task governor $task->setTemplate(false); // the task is no longer a template $task->randomize(); // randomize } @@ -79,7 +80,7 @@ class Test extends AutoStoring $this->endTime = $a["end_time"] ?? 0; $this->endLimitTime = $a["end_limit_time"] ?? 0; $this->repeatable = $a["repeatable"]; - $this->tasks = TaskFactory::constructFromCollection($a["challenges"]); + $this->tasks = TaskFactory::constructFromCollection($a["challenges"], $this); if (isset($a["summary"])) { $this->summary = TestSummary::fromArray($a["summary"]); } else { // backward compatibility @@ -123,11 +124,11 @@ class Test extends AutoStoring } // Convert test to array. - function toArray(array $omit = []): array + function toArray(array $omit = [], string $mode = "all"): array { $tasks = []; foreach ($this->tasks as &$t) { - $tasks[] = $t->toArray(); + $tasks[] = $t->toArray($mode); } $a = [ @@ -195,6 +196,7 @@ class Test extends AutoStoring // summarize points $mark_sum = 0.0; foreach ($this->tasks as &$ch) { + $ch->autoCheck(); $mark_sum += $ch->getMark(); } @@ -248,4 +250,8 @@ class Test extends AutoStoring { 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/Utils.php b/class/Utils.php new file mode 100644 index 0000000..f494eb8 --- /dev/null +++ b/class/Utils.php @@ -0,0 +1,20 @@ +verilog_form, $lf->tex_form); - $lf->getTruthTable(); + $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/composer.json b/composer.json index 3939dcd..a7fb618 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "ext-mbstring" : "*", "ext-zip": "*", "ext-fileinfo": "*", + "ext-luasandbox": "*", "phpoffice/phpspreadsheet": "^5.1", "symfony/expression-language": "^7.3" } diff --git a/interface.php b/interface.php index 714e2c1..b3d5810 100644 --- a/interface.php +++ b/interface.php @@ -258,7 +258,7 @@ function get_player_test(ReqHandler &$rh, array $params): array $test = access_test_data($params["testid"]); if ($test !== null) { - $test_data_with_current_time = $test->toArray(); + $test_data_with_current_time = $test->toArray(mode: "public"); if ($test->isOngoing()) { exclude_correct_answers($test_data_with_current_time["challenges"]); } @@ -897,7 +897,8 @@ function import_users_from_csv(ReqHandler &$rh, array $params): string function execute_cli_command(ReqHandler &$rh, array $params): string { $args = $params["cmd"]; - $cmdline = "php cli_actions.php $args"; + $phpargs = "-dxdebug.default_enable=1 -dxdebug.remote_enable=1 -dxdebug.remote_autostart=1 -dxdebug.remote_port=9001 -dxdebug.remote_host=127.0.0.1 -dxdebug.remote_mode=req -dxdebug.idekey=PHPSTORM -dxdebug.mode=debug -dxdebug.discover_client_host=true -dxdebug.start_with_request=yes"; + $cmdline = "php $phpargs cli_actions.php $args"; $resp = "=> " . $cmdline . "\n\n"; $resp .= shell_exec($cmdline); //$resp = shell_exec("php -v"); diff --git a/js/default_frame.js b/js/default_frame.js index 54adaef..11594ad 100644 --- a/js/default_frame.js +++ b/js/default_frame.js @@ -88,7 +88,7 @@ function list_corresponding_results(gameid) { let test_summary_record = document.createElement("section"); test_summary_record.classList.add("test-summary-record"); test_summary_record.addEventListener("click", () => { - open_test(record["testid"]); + open_test(record["testid"], gameid); }); let sequence_number_sec = document.createElement("section"); diff --git a/js/tasks.js b/js/tasks.js index 5686a25..b62e8be 100644 --- a/js/tasks.js +++ b/js/tasks.js @@ -1,5 +1,6 @@ class Task extends HTMLElement { static sequence_number = 0; + constructor(type) { super(); @@ -9,6 +10,8 @@ class Task extends HTMLElement { this.view_only = false; this.upload_answer_cb = null; this.player_answer = null; + this.correct_answer = null; + this.shadow = this.attachShadow({mode: "open"}); this.createStyle(); @@ -44,14 +47,28 @@ class Task extends HTMLElement { border-bottom-right-radius: 0.3em; border-top-left-radius: 0.3em; width: 2em; + z-index: 10; } section.answer-container { /* (empty) */ } + section.bad-answer { + background-color: #e5d8d3; + } code { font-family: 'Monaco', monospace; } + section#correct-answer { + display: none; + width: 100%; + margin-top: 0.5em; + } + + section#correct-answer[visible="true"] { + display: block; + } + @media only screen and (max-width: 800px) { section.task { width: calc(100vw - 3em); @@ -73,11 +90,14 @@ class Task extends HTMLElement { answer_container.classList.add("answer-container"); let seq_num_section = document.createElement("section"); seq_num_section.classList.add("seq-num"); - seq_num_section.innerText = `${this.sequence_number+1}.`; + seq_num_section.innerText = `${this.sequence_number + 1}.`; + let ca_section = document.createElement("section"); + ca_section.id = "correct-answer"; task_box.append(question_span); task_box.append(answer_container); task_box.append(seq_num_section); + task_box.append(ca_section); this.shadow.append(task_box); @@ -85,11 +105,14 @@ class Task extends HTMLElement { this.question_span = question_span; this.answer_container = answer_container; this.seq_num_section = seq_num_section; + this.ca_section = ca_section; } - connectedCallback() {} + connectedCallback() { + } - disconnectedCallback() {} + disconnectedCallback() { + } get type() { return this.task_type; @@ -113,6 +136,7 @@ class Task extends HTMLElement { set isConcluded(concluded) { this.concluded = concluded; + this.ca_section.setAttribute("visible", this.concluded ? "true" : "false"); } get isViewOnly() { @@ -135,6 +159,14 @@ class Task extends HTMLElement { return this.player_answer; } + set correctAnswer(correct_answer) { + this.correct_answer = correct_answer; + } + + get correctAnswer() { + return this.correct_answer; + } + set uploadAnswerCb(cb) { this.upload_answer_cb = cb; } @@ -145,22 +177,39 @@ class Task extends HTMLElement { } } + displayCorrectAnswer() { + + } + fromArray(a) { this.setQuestion(a["question"]); this.playerAnswer = a["player_answer"]; + this.correctAnswer = a["correct_answer"]; + + let mark = a["mark"]; + if ((mark !== undefined) && (mark !== null) && (mark > -1) && (mark !== a["max_mark"])) { + this.task_box.classList.add("bad-answer"); + } + + if (this.isConcluded) { + this.displayCorrectAnswer(); + } } } class PicturedTask extends Task { constructor(type) { super(type); + + this.img = null; + this.img_type = "none"; } createStyle() { super.createStyle(); this.css.innerHTML += ` - img.question-image { + .question-image { display: none; position: relative; margin: 1em auto; @@ -172,23 +221,45 @@ class PicturedTask extends Task { createElements() { super.createElements(); - - this.img = document.createElement("img"); - this.img.classList.add("question-image"); - this.img.src = ""; - - this.task_box.insertBefore(this.img, this.question_span); } - set imgUrl(url) { - url = url.trim(); - this.img.src = url.trim(); - this.img.style.display = (url !== "") ? "block" : "none"; + set imgData(data) { + switch (this.img_type) { + case "url": + { + this.img = document.createElement("img"); + this.img.classList.add("question-image"); + + data = data.trim(); + this.img.src = data.trim(); + } + break; + case "svg": + { + this.img = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.img.classList.add("question-image"); + this.img.innerHTML = data; + } + break; + } + + if (this.img != null) { + this.img.style.display = (data !== "") ? "block" : "none"; + this.task_box.insertBefore(this.img, this.question_span); + } } - get imgUrl() { + get imgData() { return this.img.src; } + + set imgType(t) { + this.img_type = t; + } + + get imgType() { + return this.img_type; + } } class SingleChoiceTask extends PicturedTask { @@ -227,9 +298,6 @@ class SingleChoiceTask extends PicturedTask { background-color: #176767; color: whitesmoke; } - section.bad-answer { - background-color: #e5d8d3; - } section.bad-answer section.answer input[type="radio"]:checked+label:not(.correct-answer) { background-color: #aa8a7d; } @@ -288,15 +356,7 @@ class SingleChoiceTask extends PicturedTask { this.answer_container.append(answer_section); }); - MathJax.typeset([ this.task_box ]); - } - - set correctAnswer(correct_answer) { - this.correct_answer = correct_answer; - } - - get correctAnswer() { - return this.correct_answer; + MathJax.typeset([this.task_box]); } get isCorrect() { @@ -306,7 +366,6 @@ class SingleChoiceTask extends PicturedTask { fromArray(a) { super.fromArray(a); - this.correctAnswer = a["correct_answer"]; this.setAnswers(a["answers"]); } @@ -355,6 +414,10 @@ class OpenEndedTask extends PicturedTask { this.answer_tf = answer_tf; } + displayCorrectAnswer() { + this.ca_section.innerHTML = "Lehetséges megoldások:
" + this.correctAnswer.join(", VAGY
"); + } + fromArray(a) { super.fromArray(a); } @@ -439,15 +502,23 @@ class NumberConversionTask extends OpenEndedTask { }) } - fromArray(a) { - super.fromArray(a); + displayCorrectAnswer() { + this.ca_section.innerHTML = `Megoldás: ${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]; + 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; @@ -459,7 +530,378 @@ class NumberConversionTask extends OpenEndedTask { } } +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: 'Monaco', 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"); + } +} + +class TruthTableTask extends PicturedTask { + constructor() { + super("truthtable"); + + this.input_variables = []; + this.output_variable = ""; + this.output_switches = []; + } + + createStyle() { + super.createStyle(); + + this.css.innerHTML += ` + table#tt { + border: 1.5pt solid #176767; + margin: 0.5em auto; + border-spacing: 0; + font-family: 'Monaco', 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; + } + `; + } + + 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 = "" + for (let i = 0; i < N; i++) { + inside += "" + this.input_variables[i] + ""; + } + inside += "" + this.output_variable + ""; + inside += ""; + for (let i = 0; i < M; i++) { + inside += ""; + for (let j = 0; j < N; j++) { + inside += "" + ((i >> (N - j - 1)) & 1).toString() + ""; + } + inside += "" + inside += ""; + } + + this.tt.innerHTML = inside; + + for (let i = 0; i < M; i++) { + let sw = this.shadow.getElementById("out_" + i); + sw.addEventListener("change", () => { + this.updatePlayerAnswer(); + this.uploadAnswer(); + }); + sw.setDisabled(this.isConcluded || this.isViewOnly); + this.output_switches[i] = sw; + } + + this.playerAnswer = "-".repeat(M); + } + + set playerAnswer(playerAnswer) { + if (playerAnswer !== null) { + super.playerAnswer = playerAnswer; + } + + for (let i = 0; i < this.output_switches.length; i++) { + let sw = this.output_switches[i]; + let pac = this.playerAnswer.charAt(i); + if (!this.isConcluded) { + sw.setState(pac); + } else { + let cac = this.correctAnswer.charAt(i); + if (cac !== pac) { + sw.setHighlight(pac); + } + sw.setState(cac); + } + } + } + + get playerAnswer() { + return super.playerAnswer; + } + + fromArray(a) { + super.fromArray(a); + + this.input_variables = a["input_variables"]; + this.output_variable = a["output_variable"]; + + this.buildTTable(); + + this.playerAnswer = a["player_answer"]; + } +} + +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["explanation"]; + } + + 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('truthtable-task', TruthTableTask); +customElements.define('slide-switch', Switch); +customElements.define('verilog-task', VerilogTask); diff --git a/js/terminal.js b/js/terminal.js index b3cd97a..dcb5f9f 100644 --- a/js/terminal.js +++ b/js/terminal.js @@ -10,7 +10,7 @@ function submit_command() { request(req).then((resp) => { terminal_output.value += resp + "\n\n"; terminal_output.scrollTo(0, terminal_output.scrollHeight); - terminal_input.value = ""; + //terminal_input.value = ""; terminal_input.disabled = false; }); } diff --git a/js/testground.js b/js/testground.js index 190abd1..a3b0eca 100644 --- a/js/testground.js +++ b/js/testground.js @@ -74,12 +74,17 @@ function populate_tasks(tasks, concluded, view_only = false, gameid) { let test_display = document.getElementById("test_display"); test_display.innerHTML = ""; + Task.sequence_number = 0; + tasks.forEach((task) => { let task_element = document.createElement(`${task["type"]}-task`); task_element.uploadAnswerCb = save_answer; - if (task["image_url"] !== "") { - task_element.imgUrl = `interface.php?action=get_image&gameid=${gameid}&img_url=${task["image_url"]}` + 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"]; } task_element.isConcluded = concluded; task_element.isViewOnly = view_only; diff --git a/style/spreadquiz.css b/style/spreadquiz.css index 84b14c5..29b2320 100644 --- a/style/spreadquiz.css +++ b/style/spreadquiz.css @@ -1,5 +1,6 @@ @import url('https://fonts.googleapis.com/css2?family=Autour+One&family=Kanit:wght@500&display=swap'); @import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"); +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap'); body { font-family: 'Autour One', sans-serif; diff --git a/testground.php b/testground.php index 581b8e1..1f1ea41 100644 --- a/testground.php +++ b/testground.php @@ -36,6 +36,7 @@ if ($testid === "") { +
@@ -68,7 +69,7 @@ if ($testid === "") {