SpreadQuiz/class/GameMgr.php
Epagris e313923ad4 - checking maintenance in each file added
- a few CLI commands added
- ticking and concluding expired tests added
2024-09-26 11:34:02 +02:00

434 lines
13 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 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;
}
// -------
}