false, // player may traverse back and forth between challenges "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 $challengesLoaded; // Indicates if challenges have been fetched private array $challenges; // Challenges // ------- static private function genPublicId(): string { return uniqid("p"); } // ------- static private function patchUpGameDate(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 challenges. public function loadChallenges(): void { if ($this->isGameFileIsPresent() && !$this->challengesLoaded) { // load if file is present $this->challenges = json_decode(file_get_contents($this->getGameFile()), true); } } // Save challenges. public function saveChallenges(): void { file_put_contents($this->getGameFile(), json_encode($this->challenges)); // store challenges 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->challengesLoaded = 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->challenges = []; } // Create game from array representation. static function fromArray(GameMgr &$gameMgr, array $a): Game { $id = $a["_id"] ?? -1; self::patchUpGameDate($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 challenges to a CSV file. function exportChallengesToCSV(&$f): void { // load challenges $this->loadChallenges(); // populate CSV file foreach ($this->challenges as $ch) { $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 GAMEMEDIA_DIR . DIRECTORY_SEPARATOR . $this->getId(); } // 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 challenges from a CSV table. function importChallengesFromCSV(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 challenges $this->challenges = []; // 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 challenge record $ch = [ "question" => trim($csvline[0]), "image_url" => trim($csvline[1]), "correct_answer" => trim($csvline[2]), "answers" => array_filter(array_slice($csvline, 2), function ($v) { return trim($v) !== ""; }) ]; // if image is attached to the challenge, then give a random name to the image if ($ch["image_url"] !== "") { $old_img_name = $ch["image_url"]; $ext = pathinfo($old_img_name, PATHINFO_EXTENSION); $ext = ($ext !== "") ? ("." . $ext) : $ext; $new_img_name = uniqid("img_", true) . $ext; $ch["image_url"] = $new_img_name; // 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); } // store the challenge $this->challenges[] = $ch; } } fclose($f); // save challenges $this->saveChallenges(); // update game with game file present $this->gameFileIsPresent = true; // store modifications $this->commitMods(); return ["n" => count($this->challenges), "encoding" => $encoding]; } // --------- 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 getChallenges(): array { $this->loadChallenges(); return $this->challenges; } } class GameMgr { private \SleekDB\Store $db; // game database // -------- function __construct() { $this->db = new \SleekDB\Store(GAMEDB, DATADIR, ["timeout" => false]); } // Get game by ID. function getGame(string $gameid): Game|null { $game_data_array = $this->db->findById($gameid); return count($game_data_array) != 0 ? Game::fromArray($this, $game_data_array) : null; } // Get public game. FIXME!!! function getPublicGame(string $public_id): Game|null { $game_data_array = $this->db->findBy([["public", "=", "true"], "AND", ["public_id", "=", $public_id]]); return count($game_data_array) != 0 ? Game::fromArray($this, $game_data_array[0]) : null; } // Update game. function updateGame(Game $game): void { $a = $game->toArray(); $this->db->update($a); } function addGame(string $name, string $owner, string $description, array $properties = Game::DEFAULT_GAME_PROPERTIES, array $contributors = [], array $challenges = []): bool { $game_data = [ "name" => $name, "owner" => $owner, "contributors" => $contributors, "description" => $description, "game_file_present" => false, "properties" => $properties, "public" => false, "public_id" => self::genPublicId(), "version" => Game::CURRENT_GAME_VERSION ]; $game_data = $this->db->insert($game_data); // prepare game context $game = Game::fromArray($this, $game_data); $current_game_media_dir = $game->getGameDir(); mkdir($current_game_media_dir); $game->saveChallenges(); return true; } // Delete game by ID. function deleteGame(string $gameid): void { $this->db->deleteById($gameid); } // Get all game data by contributor nickname. function getAllGameDataByContributor(string $nickname): array { $games = []; if ($nickname !== "*") { $game_data_array = $this->db->findBy([["owner", "=", $nickname], "OR", ["contributors", "CONTAINS", $nickname]]); } else { $game_data_array = $this->db->findAll(); } foreach ($game_data_array as $game_data) { $games[] = Game::fromArray($this, $game_data); } return $games; } // Get all games. function getAllGames() : array { $gamesa = $this->db->findAll(); $games = []; foreach ($gamesa as $a) { $games[] = new Game($this, $a); } return $games; } // ------- }