464 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			464 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
require_once "vendor/autoload.php";
 | 
						|
 | 
						|
require_once "AutoStoring.php";
 | 
						|
 | 
						|
class Game extends AutoStoring
 | 
						|
{
 | 
						|
    public const DEFAULT_GAME_PROPERTIES = [
 | 
						|
        "forward_only" => 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 public 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" => Game::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;
 | 
						|
    }
 | 
						|
 | 
						|
    // Sanitize games and return game IDs.
 | 
						|
    function sanitizeGames(array $games) : array {
 | 
						|
        $sanitized = [];
 | 
						|
        foreach ($games as $game) {
 | 
						|
            // explode game identifier
 | 
						|
            [$name, $id] = explode("#", $game);
 | 
						|
 | 
						|
            // fetch game
 | 
						|
            if ($id !== null) {
 | 
						|
                $records = $this->db->findBy([["name", "=", $name], "AND", ["_id", "=", (int)$id]]);
 | 
						|
            } else {
 | 
						|
                $records = $this->db->findBy(["name", "=", $name]);
 | 
						|
            }
 | 
						|
 | 
						|
            // put game ID into sanitized list only if identifier is not ambiguous
 | 
						|
            if (count($records) === 1) {
 | 
						|
                $sanitized[] = $records[0]["_id"];
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $sanitized;
 | 
						|
    }
 | 
						|
 | 
						|
    // Resolve group IDs to full group identifiers.
 | 
						|
    function resolveGames(array &$gameIds): void {
 | 
						|
        $a = $this->db->findBy([["_id", "IN", $gameIds]]); // no caching here...
 | 
						|
        $gameIds = array_map(fn($r) => $r["name"] . "#" . $r["_id"] ,$a);
 | 
						|
    }
 | 
						|
 | 
						|
    // -------
 | 
						|
}
 |