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 = "