572 lines
18 KiB
PHP
572 lines
18 KiB
PHP
<?php
|
|
|
|
require_once "vendor/autoload.php";
|
|
|
|
require_once "AutoStoring.php";
|
|
|
|
require_once "Utils.php";
|
|
|
|
class Game extends AutoStoring
|
|
{
|
|
public const DEFAULT_GAME_PROPERTIES = [
|
|
"forward_only" => false, // player may traverse back and forth between tasks
|
|
"time_limit" => 0, // no time limit; otherwise, this field indicates time limit in seconds
|
|
"repeatable" => false // this test can be taken multiple times
|
|
];
|
|
|
|
public const CURRENT_GAME_VERSION = 2; // MUST BE INCREMENTED!!
|
|
|
|
// --------
|
|
private int $id; // Game's ID
|
|
private string $name; // Game's name
|
|
private string $owner; // Game's owner
|
|
private array $contributors; // Contributors to the game
|
|
private string $description; // Game's description
|
|
private bool $gameFileIsPresent; // Indicates if game CSV is in place
|
|
private array $properties; // Collection of several game properties
|
|
private bool $public; // Is this game publicly available?
|
|
private string $publicId; // Public-accessible ID
|
|
private int $VERSION; // Game representation version (used during updates)
|
|
private GameMgr $gameMgr; // Game manager managing this instance
|
|
private bool $tasksLoaded; // Indicates if tasks have been fetched
|
|
private array $tasks; // Tasks
|
|
|
|
// -------
|
|
|
|
static public function genPublicId(): string
|
|
{
|
|
return uniqid("p");
|
|
}
|
|
|
|
// -------
|
|
|
|
static private function patchUpGameData(array &$a): void
|
|
{
|
|
$version = $a["version"] ?? 0;
|
|
if ($version < 2) { // update to game version 2
|
|
if (!key_exists("public_id", $a)) {
|
|
$a["public"] = false;
|
|
$a["public_id"] = self::genPublicId();
|
|
}
|
|
|
|
$a["version"] = 2;
|
|
}
|
|
}
|
|
|
|
// Store modifications.
|
|
public function storeMods(): void
|
|
{
|
|
$this->gameMgr->updateGame($this);
|
|
}
|
|
|
|
// Commit modifications.
|
|
function commitMods(): void
|
|
{
|
|
//$this->patchUpGameDate();
|
|
parent::commitMods();
|
|
}
|
|
|
|
// Load game tasks.
|
|
public function loadTasks(): void
|
|
{
|
|
if ($this->isGameFileIsPresent() && !$this->tasksLoaded) { // load if file is present
|
|
$this->tasks = TaskFactory::constructFromCollection(json_decode(file_get_contents($this->getGameFile()), true));
|
|
foreach ($this->tasks as &$ch) {
|
|
$ch->setTemplate(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save tasks.
|
|
public function saveTasks(): void
|
|
{
|
|
file_put_contents($this->getGameFile(), json_encode($this->tasks)); // store tasks in JSON-format
|
|
}
|
|
|
|
// -------
|
|
|
|
function __construct(GameMgr &$gameMgr, string $name, string $description = "", int $id = -1, string $owner = "",
|
|
array $contributors = [], bool $gameFileIsPresent = false, array $properties = [],
|
|
bool $public = false, string $publicId = "", int $version = 2)
|
|
{
|
|
parent::__construct();
|
|
|
|
$this->tasksLoaded = false;
|
|
|
|
$this->gameMgr = $gameMgr;
|
|
$this->id = $id;
|
|
$this->name = $name;
|
|
$this->description = $description;
|
|
$this->owner = $owner;
|
|
$this->contributors = $contributors;
|
|
$this->gameFileIsPresent = $gameFileIsPresent;
|
|
$this->properties = $properties;
|
|
$this->public = $public;
|
|
$this->publicId = $publicId;
|
|
$this->VERSION = $version;
|
|
$this->tasks = [];
|
|
}
|
|
|
|
// Create game from array representation.
|
|
static function fromArray(GameMgr &$gameMgr, array $a): Game
|
|
{
|
|
$id = $a["_id"] ?? -1;
|
|
self::patchUpGameData($a);
|
|
return new Game($gameMgr, $a["name"], $a["description"], $id, $a["owner"], $a["contributors"],
|
|
$a["game_file_present"], $a["properties"], $a["public"], $a["public_id"], $a["version"]);
|
|
}
|
|
|
|
const OMIT_ADVANCED_FIELDS = ["contributors", "game_file_is_present", "properties", "public", "public_id", "version"];
|
|
|
|
// Convert game to array representation.
|
|
function toArray(array $omit = []): array
|
|
{
|
|
$a = [
|
|
"_id" => $this->id,
|
|
"name" => $this->name,
|
|
"description" => $this->description,
|
|
"owner" => $this->owner,
|
|
"contributors" => $this->contributors,
|
|
"game_file_present" => $this->gameFileIsPresent,
|
|
"properties" => $this->properties,
|
|
"public" => $this->public,
|
|
"public_id" => $this->publicId,
|
|
"version" => $this->VERSION,
|
|
];
|
|
|
|
foreach ($omit as $field) {
|
|
unset($a[$field]);
|
|
}
|
|
|
|
return $a;
|
|
}
|
|
|
|
// Export tasks to a CSV file. TODO: ez csak a feleletválasztóshoz lesz jó
|
|
function exportTasksToCSV(&$f): void
|
|
{
|
|
// load tasks
|
|
$this->loadTasks();
|
|
|
|
// populate CSV file
|
|
foreach ($this->tasks as $ch) {
|
|
if ($ch->getType() === "singlechoice") {
|
|
$csvline = [
|
|
$ch["question"],
|
|
$ch["image_url"],
|
|
];
|
|
$csvline = array_merge($csvline, $ch["answers"]);
|
|
fputcsv($f, $csvline);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get game directory NAME with path. Does not check if the game directory exists or not.
|
|
function getGameDir(): string
|
|
{
|
|
return self::getGameDirById($this->getId());
|
|
}
|
|
|
|
function getAccompanyingFilePath(string $file_name): string {
|
|
return $this->getGameDir() . DIRECTORY_SEPARATOR . $file_name;
|
|
}
|
|
|
|
static function getGameDirById(int $id): string {
|
|
return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $id;
|
|
}
|
|
|
|
// Get game file NAME with path. Does not check whether the game file is in place or not.
|
|
function getGameFile(): string
|
|
{
|
|
return GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId() . DIRECTORY_SEPARATOR . GAME_FILE;
|
|
}
|
|
|
|
// Is the given user the owner of the game?
|
|
function isUserOwner(string $nickname): bool
|
|
{
|
|
return $this->owner === $nickname;
|
|
}
|
|
|
|
// Is the given user a contributor of the game?
|
|
function isUserContributor(string $nickname): bool
|
|
{
|
|
return in_array($nickname, $this->contributors);
|
|
}
|
|
|
|
// Is user contributor or owner?
|
|
function isUserContributorOrOwner(string $nickname): bool
|
|
{
|
|
return $this->isUserContributor($nickname) || $this->isUserOwner($nickname);
|
|
}
|
|
|
|
const CSV_ENCODINGS = ["UTF-8", "Windows-1252"];
|
|
|
|
// Import tasks from a CSV table. TODO: ez csak a feleletválasztós betöltésére lesz jó
|
|
function importTasksFromCSV(string $csv_path): array
|
|
{
|
|
// convert text encoding into UTF-8
|
|
$data = file_get_contents($csv_path);
|
|
$encoding = "UNKNOWN";
|
|
foreach (self::CSV_ENCODINGS as $enc) { // detect encoding
|
|
if (mb_check_encoding($data, $enc)) {
|
|
$encoding = $enc;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($encoding !== "UNKNOWN") { // if encoding has been detected successfully
|
|
$data = mb_convert_encoding($data, "UTF-8", $encoding);
|
|
file_put_contents($csv_path, $data);
|
|
}
|
|
|
|
// clear tasks
|
|
$this->tasks = [];
|
|
|
|
// load filled CSV file
|
|
$f = fopen($csv_path, "r");
|
|
if (!$f) { // failed to open file
|
|
return ["n" => 0, "encoding" => $encoding];
|
|
}
|
|
while ($csvline = fgetcsv($f)) {
|
|
// skip empty lines
|
|
if (trim(implode("", $csvline)) === "") {
|
|
continue;
|
|
}
|
|
if (count($csvline) >= 3) {
|
|
// construct task record
|
|
$a = [
|
|
"question" => trim($csvline[0]),
|
|
"image_data" => trim($csvline[1]),
|
|
"correct_answer" => 0,
|
|
"answers" => array_filter(array_slice($csvline, 2), function ($v) {
|
|
return trim($v) !== "";
|
|
})
|
|
];
|
|
|
|
// if an image is attached to the task, then give a random name to the image
|
|
if ($a["image_data"] !== "") {
|
|
$a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]);
|
|
$a["image_type"] = "url";
|
|
}
|
|
|
|
// store the task
|
|
$this->tasks[] = new SingleChoiceTask($a);
|
|
}
|
|
}
|
|
fclose($f);
|
|
|
|
// save tasks
|
|
$this->saveTasks();
|
|
|
|
// update game with game file present
|
|
$this->gameFileIsPresent = true;
|
|
|
|
// store modifications
|
|
$this->commitMods();
|
|
|
|
return ["n" => count($this->tasks), "encoding" => $encoding];
|
|
}
|
|
|
|
private static function getTableVersion(string &$cA1): string
|
|
{
|
|
if (str_starts_with($cA1, "#:V")) {
|
|
return trim(substr($cA1, 3));
|
|
} else {
|
|
return "1";
|
|
}
|
|
}
|
|
|
|
private function obfuscateAttachedImage(string $old_img_name): string
|
|
{
|
|
$ext = pathinfo($old_img_name, PATHINFO_EXTENSION);
|
|
$ext = ($ext !== "") ? ("." . $ext) : $ext;
|
|
$new_img_name = uniqid("img_", true) . $ext;
|
|
|
|
// rename the actual file
|
|
$old_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $old_img_name;
|
|
$new_img_path = $this->getGameDir() . DIRECTORY_SEPARATOR . $new_img_name;
|
|
rename($old_img_path, $new_img_path);
|
|
|
|
return $new_img_name;
|
|
}
|
|
|
|
private function importTasksFromTableV1(array &$table): array
|
|
{
|
|
$n = count($table);
|
|
for ($i = 1; $i < $n; $i++) {
|
|
$row = &$table[$i]; // fetch row
|
|
$a = [ // create initializing array
|
|
"question" => trim($row[0]),
|
|
"image_data" => trim($row[1]),
|
|
"correct_answer" => 0,
|
|
"answers" => array_filter(array_slice($row, 2), function ($v) {
|
|
return trim($v ?? "") !== "";
|
|
})
|
|
];
|
|
|
|
// obfuscate image filename
|
|
if ($a["image_data"] !== "") {
|
|
$a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]);
|
|
$a["image_type"] = "url";
|
|
}
|
|
|
|
// create the task
|
|
$this->tasks[] = new SingleChoiceTask($a);
|
|
}
|
|
|
|
return ["n" => $n, "encoding" => "automatikusan konvertált"];
|
|
}
|
|
|
|
private static function getTableColumnIndices(array &$header): array
|
|
{
|
|
$columns = [];
|
|
for ($i = 1; $i < count($header); $i++) { // skip the first column as it is metadata
|
|
$label = $header[$i];
|
|
if (($label ?? "") !== "") {
|
|
$columns[$label] = $i;
|
|
}
|
|
}
|
|
return $columns;
|
|
}
|
|
|
|
private static function getFirstUnlabeledColumn(array &$header): int
|
|
{
|
|
for ($i = 0; $i < count($header); $i++) {
|
|
if (trim($header[$i] ?? "") === "") {
|
|
return $i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static function explodeFlags(string $fs): array
|
|
{
|
|
$flags = explode(",", trim($fs));
|
|
return array_filter($flags, fn($v) => trim($v) !== "");
|
|
}
|
|
|
|
private function importTasksFromTableV2(array &$table): array
|
|
{
|
|
$result = ["n" => 0, "encoding" => "automatikusan konvertált"]; // prepare result
|
|
$n = count($table); // get number of entries (including header)
|
|
if ($n === 0) { // cannot import an empty table
|
|
return $result;
|
|
}
|
|
$header = &$table[0]; // extract header
|
|
|
|
$fuc = Game::getFirstUnlabeledColumn($header); // get first unlabeled column
|
|
if ($fuc === -1) { // if there's no data, then it is impossible to create the tasks
|
|
return $result;
|
|
}
|
|
|
|
$columns = Game::getTableColumnIndices($header); // fetch column names
|
|
|
|
// start iterating over tasks
|
|
$count = 0;
|
|
for ($i = 1; $i < $n; $i++) {
|
|
$row = &$table[$i]; // fetch row
|
|
|
|
$flags = Game::explodeFlags($row[0] ?? ""); // get flags
|
|
if (in_array("hidden", $flags)) { // skip hidden tasks
|
|
continue;
|
|
}
|
|
|
|
// count in this task
|
|
$count++;
|
|
|
|
// prepare a function that looks up the fields referenced by their labels
|
|
$select_fn = function (array $cols) use (&$row, &$columns) {
|
|
for ($i = 0; $i < count($cols); $i++) {
|
|
if (isset($columns[$cols[$i]])) {
|
|
return trim($row[$columns[$cols[$i]]]);
|
|
}
|
|
}
|
|
return "";
|
|
};
|
|
|
|
// prepare a function that extracts all unlabeled fields
|
|
$extract_unlabeled_fn = fn() => array_filter(array_slice($row, $fuc), function ($v) {
|
|
return trim($v ?? "") !== "";
|
|
});
|
|
|
|
// fetch generic fields
|
|
$a = [
|
|
"flags" => $flags,
|
|
"type" => strtolower($select_fn(["Típus", "Type"])),
|
|
"image_data" => $select_fn(["Kép", "Image"]),
|
|
"question" => $select_fn(["Kérdés", "Question"]),
|
|
"lua_script" => $select_fn(["Lua"]),
|
|
"lua_params" => Utils::str2kv($select_fn(["LuaParam"])),
|
|
];
|
|
|
|
// convert into
|
|
switch ($a["type"]) {
|
|
case "singlechoice":
|
|
$a["answers"] = $extract_unlabeled_fn();
|
|
$a["correct_answer"] = 0;
|
|
break;
|
|
case "openended":
|
|
$a["correct_answer"] = $extract_unlabeled_fn();
|
|
break;
|
|
case "numberconversion":
|
|
$a["instruction"] = $row[$fuc];
|
|
break;
|
|
case "truthtable":
|
|
case "logicfunction":
|
|
$a["input_variables"] = Utils::str2a($row[$fuc]);
|
|
$a["output_variable"] = $row[$fuc + 1];
|
|
$a["expression"] = $row[$fuc + 2];
|
|
break;
|
|
case "verilog":
|
|
$a["test_bench_fn"] = $row[$fuc];
|
|
$a["correct_answer"] = file_get_contents($this->getAccompanyingFilePath($row[$fuc + 1]));
|
|
if (isset($row[$fuc + 2])) {
|
|
$a["player_answer"] = file_get_contents($this->getAccompanyingFilePath($row[$fuc + 2]));
|
|
} else {
|
|
$a["player_answer"] = "";
|
|
}
|
|
break;
|
|
}
|
|
|
|
// obfuscate image filename
|
|
if ($a["image_data"] !== "") {
|
|
if (in_array("codeimage", $flags)) {
|
|
$a["image_type"] = "code";
|
|
} else {
|
|
$a["image_data"] = $this->obfuscateAttachedImage($a["image_data"]);
|
|
$a["image_type"] = "url";
|
|
}
|
|
}
|
|
|
|
// generate the task
|
|
$this->tasks[] = TaskFactory::fromArray($a, $this);
|
|
|
|
// assign scoring strategy
|
|
$sct = $select_fn(["Pontozás", "Scoring"]);
|
|
if ($sct !== "") {
|
|
$sct_fields = Utils::str2kv($sct);
|
|
foreach ($sct_fields as $key => $value) {
|
|
$sct_fields[$key] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
$result["n"] = $count;
|
|
return $result;
|
|
}
|
|
|
|
public function importTasksFromTable(array &$table): array
|
|
{
|
|
// clear tasks
|
|
$this->tasks = [];
|
|
|
|
// get table version
|
|
$vs = Game::getTableVersion($table[0][0]);
|
|
|
|
// continue processing based on table version
|
|
$result = ["n" => 0, "encoding" => "ismeretlen"];
|
|
switch ($vs) {
|
|
case "1":
|
|
$result = $this->importTasksFromTableV1($table);
|
|
break;
|
|
case "2":
|
|
$result = $this->importTasksFromTableV2($table);
|
|
break;
|
|
}
|
|
|
|
// if the number of imported tasks is not zero, then it was a successful import
|
|
$this->gameFileIsPresent = false; // assume no game file present
|
|
if ($result["n"] > 0) {
|
|
$this->saveTasks(); // save tasks
|
|
$this->gameFileIsPresent = true; // update game with game file present
|
|
$this->commitMods(); // store modifications
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
// ---------
|
|
|
|
public function getName(): string
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
public function setName(string $name): void
|
|
{
|
|
$this->name = $name;
|
|
}
|
|
|
|
public function getOwner(): string
|
|
{
|
|
return $this->owner;
|
|
}
|
|
|
|
public function setOwner(string $owner): void
|
|
{
|
|
$this->owner = $owner;
|
|
}
|
|
|
|
public function getContributors(): array
|
|
{
|
|
return $this->contributors;
|
|
}
|
|
|
|
public function setContributors(array $contributors): void
|
|
{
|
|
$this->contributors = $contributors;
|
|
}
|
|
|
|
public function getDescription(): string
|
|
{
|
|
return $this->description;
|
|
}
|
|
|
|
public function setDescription(string $description): void
|
|
{
|
|
$this->description = $description;
|
|
}
|
|
|
|
public function getId(): int
|
|
{
|
|
return $this->id;
|
|
}
|
|
|
|
public function isGameFileIsPresent(): bool
|
|
{
|
|
return $this->gameFileIsPresent;
|
|
}
|
|
|
|
function setProperties(array $properties): void
|
|
{
|
|
$this->properties = $properties;
|
|
$this->commitMods();
|
|
}
|
|
|
|
public function& getProperties(): array
|
|
{
|
|
return $this->properties;
|
|
}
|
|
|
|
public function isPublic(): bool
|
|
{
|
|
return $this->public;
|
|
}
|
|
|
|
public function getPublicId(): string
|
|
{
|
|
return $this->publicId;
|
|
}
|
|
|
|
public function setPublic(bool $public): void
|
|
{
|
|
$this->public = $public;
|
|
$this->commitMods();
|
|
}
|
|
|
|
public function getTasks(): array
|
|
{
|
|
$this->loadTasks();
|
|
return $this->tasks;
|
|
}
|
|
} |