429 lines
12 KiB
PHP
429 lines
12 KiB
PHP
<?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 array $challenges; // Challenges
|
|
|
|
// -------
|
|
|
|
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"] = generate_public_id();
|
|
}
|
|
|
|
$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()) { // 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->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", "groups", "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
|
|
{
|
|
return $this->challenges;
|
|
}
|
|
}
|
|
|
|
class GameMgr
|
|
{
|
|
private \SleekDB\Store $db; // game database
|
|
|
|
// --------
|
|
|
|
static private function genPublicId(): string
|
|
{
|
|
return uniqid("p");
|
|
}
|
|
|
|
// --------
|
|
|
|
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,
|
|
"groups" => [],
|
|
"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;
|
|
}
|
|
|
|
// -------
|
|
} |