This commit is contained in:
Wiesner András 2025-10-11 18:11:21 +02:00
parent 1fa2924abd
commit 0e0490eafb
24 changed files with 1178 additions and 233 deletions

View File

@ -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;

View File

@ -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);
}
}
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: ([\/!~]*(<input vars>)[&]{1,2})*([\/!~]*(<input vars>))
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();

54
class/LogicUtils.php Normal file
View File

@ -0,0 +1,54 @@
<?php
class LogicUtils
{
public static function extend(string $num, int $base, int $exnd, bool $comp): string
{
$fd = (int)(base_convert($num[0], $base, 10)); // get first digit as a number
$extd = (string)(($comp && ($fd >= ($base / 2))) ? ($base - 1) : 0); // get the extension digit
return str_pad((string)($num), $exnd, $extd, STR_PAD_LEFT); // extend to the left
}
public static function complement(string $num, int $base): string
{
// convert to an integer
$M = (int)(base_convert($num, $base, 10));
// check if the highest digit is less than the half of the base, if not, add a zero prefix
$fd = (int)(base_convert($num[0], $base, 10));
// create the basis for forming the complement
$H = (string)($base - 1);
$K_str = (int)(str_repeat($H, strlen($num)));
if ($fd >= ($base / 2)) { // if one more digit is needed...
$K_str = $H . $K_str; // prepend with a zero digit
}
// convert to integer
$K = (int)(base_convert($K_str, $base, 10));
// form the base's complement
$C = $K - $M + 1;
// convert to the final base
return base_convert((string)$C, 10, $base);
}
public static function changeRepresentation(int $num, int $base, string $rep, int $digits): string {
$neg = $num < 0; // store if the value is negative
$numa_str = (string)(abs($num)); // create the absolute value as a string
$numa_str = base_convert($numa_str, 10, $base); // convert to specific base
if ($neg) {
if ($rep === "s") {
$numa_str = self::extend($numa_str, $base, $digits, false);
$numa_str = "-" . $numa_str;
} else if ($rep === "c") {
$numa_str = self::complement($numa_str, $rep);
$numa_str = self::extend($numa_str, $base, $digits, true);
}
} else {
$numa_str = self::extend($numa_str, $base, $digits, false);
}
return $numa_str;
}
}

19
class/PythonUtils.php Normal file
View File

@ -0,0 +1,19 @@
<?php
require_once "Utils.php";
class PythonUtils
{
private const VENV = Utils::WORKSPACE_DIR . DIRECTORY_SEPARATOR . "venv";
public static function execPy(string $script, array $args): void
{
$venv = getcwd() . DIRECTORY_SEPARATOR . self::VENV; // compose full venv path
$ws = getcwd() . DIRECTORY_SEPARATOR . Utils::WORKSPACE_DIR; // compose full workspace path
//$source_cmd = "source " . $venv . DIRECTORY_SEPARATOR . "bin" . DIRECTORY_SEPARATOR . "activate";
$flattened_args = join(" ", array_map(fn($arg) => "'$arg'", $args)); // prepare arguments for use on command line
$python_cmd = "bash $ws" . DIRECTORY_SEPARATOR . "py_exec.sh \"$ws" . DIRECTORY_SEPARATOR . $script . "\" " . $flattened_args . " 2>&1";
$ret = shell_exec($python_cmd); // execute python script
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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 = [];

View File

@ -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;
}
}

107
class/Tasks/VerilogTask.php Normal file
View File

@ -0,0 +1,107 @@
<?php
require_once "PicturedTask.php";
class VerilogTask extends PicturedTask
{
private string $test_bench_fn; // test bench file name
private string $explanation; // short explanation for the marking
private const FAILED_MARK = "[FAILED]"; // mark at the beginning of testbench results indicating a failed combination
public function __construct(array &$a = null)
{
parent::__construct("verilog", $a);
$this->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;
}
}

View File

@ -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);
}
}

20
class/Utils.php Normal file
View File

@ -0,0 +1,20 @@
<?php
class Utils
{
public const WORKSPACE_DIR = "workspace";
public static function str2kv(string $str): array
{
preg_match_all("/([^,= ]+)=([^,= ]+)/", $str, $r);
return array_combine($r[1], $r[2]);
}
public static function str2a(string $str): array {
return explode(",", $str);
}
public static function getWorkspaceDir(): string {
return getcwd() . DIRECTORY_SEPARATOR . self::WORKSPACE_DIR;
}
}

View File

@ -45,10 +45,19 @@ if (isset($options["action"])) {
break;
case "gen_random":
{
//$lf = LogicFunction::genRandom(["a", "b", "c"], 2, 4);
$lf = LogicFunction::genRandomDF(["a", "b", "c"]);
printf("Verilog-form: %s\nTeX-form: %s\n", $lf->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;
}

View File

@ -5,6 +5,7 @@
"ext-mbstring" : "*",
"ext-zip": "*",
"ext-fileinfo": "*",
"ext-luasandbox": "*",
"phpoffice/phpspreadsheet": "^5.1",
"symfony/expression-language": "^7.3"
}

View File

@ -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");

View File

@ -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");

View File

@ -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 = "<b>Lehetséges megoldások:</b><br style='margin-bottom: 0.3em'>" + this.correctAnswer.join(", <i>VAGY</i><br>");
}
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: <code>${this.correctAnswer}</code><sub>(${this.dst_base})</sub>`;
}
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"]}<sub>(${parts[1]})</sub> =`;
let dst_exp = `<sub>(${parts[4]})</sub> <i>(${parts[6]} digiten)</i>`;
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 = "<tr>"
for (let i = 0; i < N; i++) {
inside += "<th>" + this.input_variables[i] + "</th>";
}
inside += "<th>" + this.output_variable + "</th>";
inside += "</tr>";
for (let i = 0; i < M; i++) {
inside += "<tr>";
for (let j = 0; j < N; j++) {
inside += "<td>" + ((i >> (N - j - 1)) & 1).toString() + "</td>";
}
inside += "<td><slide-switch id='out_" + i + "'></slide-switch></td>"
inside += "</tr>";
}
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);

View File

@ -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;
});
}

View File

@ -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;

View File

@ -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;

View File

@ -36,6 +36,7 @@ if ($testid === "") {
<script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<script type="text/javascript" src="https://nturley.github.io/netlistsvg/elk.bundled.js"></script>
<script type="text/javascript" src="https://nturley.github.io/netlistsvg/built/netlistsvg.bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.43.3/ace.min.js" integrity="sha512-BHJlu9vUXVrcxhRwbBdNv3uTsbscp8pp3LJ5z/sw9nBJUegkNlkcZnvODRgynJWhXMCsVUGZlFuzTrr5I2X3sQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
<section id="test_display">
@ -68,7 +69,7 @@ if ($testid === "") {
</section>
</section>
<script>
populate_all("<?=$testid ?>", "<?=$gameid ?>", <?=$view_only ?>);
populate_all("<?=$testid ?>", "<?=$gameid ?>", <?=$view_only ? "true" : "false" ?>);
window.onbeforeunload = () => {
if (TEST_DATA["state"] !== "concluded") {
save_all_answers();

3
workspace/py_exec.sh Normal file
View File

@ -0,0 +1,3 @@
source /home/epagris/VCSDEV/SpreadQuiz/workspace/venv/bin/activate
python "$@"
deactivate