Прошлым летом я публиковал статью о моем небольшом учебном проекте-игре "Слова из слова", написанном на JavaScript. Время идет и, я надеюсь, идет не напрасно. Постепенно набираясь знаний, я решил расширить идею и начать создание некого подобия интернет-площадки, которая объединит тематические игры со словами на одном ресурсе. Под катом ссылка на рабочий прототип проекта.


wordsgames.by



О проекте


Игровой сайт «Игры со словами» представляет собой платформу для размещения игр соответствующей тематики.
Зарегистрированные игроки имеют доступ к имеющимся на ресурсе играм, а также участвуют в рейтинге, формируемом на основе игрового уровня пользователя. Опыт набирается в результате прохождения различных игр и выполнения определенных задач.


Ссылка на репозиторий GitHub: https://github.com/Ghivan/wordsgames
Ссылка на рабочий прототип платформы: https://wordsgames.by/login/


Инструменты создания


Фронтэнд — Typescript, SCSS, Bootstrap, JQuery.
Бэкэнд — PHP 7, MySQL.


База данных


Схема базы данных

database schema


База данных состоит из шести таблиц:


  1. Глобальные таблицы ресурса:
    • dictionary — толковый словарь;
    • players — данные о зарегистрированных игроках;
    • games — описание игр;
  2. Таблицы игры "Слова из слова":
    • wfw_levels — информация об этапах;
    • wfw_scoreTable — очки (в будущем также и достижения) игрока ;
    • wfw_levelsPassed — информация о прохождении этапов.

Таблицы players, wfw_levelsPassed и wfw_scoreTable связаны через id игрока. Таблицы wfw_levels и wfw_levelsPassed связаны через поле word (сделано для того, чтобы можно было менять порядок уровня на обновляя записи о прохождении).
У каждого игрока есть игровой уровень (дань RPG), общий для ресурса. Количество опыта, необходимое для перехода на следующий уровень, рассчитывается по формуле геометрической прогрессии. За определение уровня игрока отвечает пользовательская функция в СУБД, а данные обновляются посредством триггера.


Функция и триггер
DELIMITER $$

CREATE FUNCTION `countExp`(`lvl` INT) RETURNS int(11)
    NO SQL
    SQL SECURITY INVOKER
    COMMENT 'Подсчитывает необходимое количество опыта для уровня'
BEGIN
  DECLARE exp int;
SET exp = FLOOR(1000 * (POW(1.1, lvl) - 1));
RETURN exp;
END$$

DELIMITER ;
-- Триггер, определяющий уровень игрока:
DELIMITER //
CREATE TRIGGER `lvlCount` BEFORE UPDATE ON `players`
 FOR EACH ROW BEGIN
IF (NEW.exp <> OLD.exp AND NEW.exp > 0) THEN
IF NEW.exp >= countExp(NEW.`level`) THEN
WHILE NEW.exp >= countExp(NEW.`level`)
  DO
    set NEW.level = NEW.level + 1;
  END WHILE;
ELSEIF  NEW.exp < countExp(NEW.`level` - 1) THEN
  WHILE NEW.exp < countExp(NEW.`level` - 1)
  DO
    set NEW.level = NEW.level - 1;
  END WHILE;
END IF;
END IF;
END
//
DELIMITER ;

Соединение с базой данных из PHP


За соединение с базой отвечает статический класс:


Класс DB
class DB
{
    private static $dbc = null;

    protected static function getConnection()
    {

        if (!self::$dbc){
            try {
                self::$dbc = new PDO("mysql:host=".HOST.";dbname=".DB_NAME.";charset=UTF8", DB_USER, DB_PASSWORD);
            } catch (Throwable $e) {
                ErrorLogger::logException($e);
                return null;
            }
        }
        return self::$dbc;
    }
}

В дальнейшем для получения информации используются классы наследники. Пример одного из классов под спойлером.


Получение информации об играх
class DBGamesGlobalInfo extends DB
{
    private static $queries = array(
        'globalInfo' => 'SELECT id, name, rules, status, author, path  FROM games'
    );

    static function getGlobalInfo(){
        try {
            $stmt = parent::getConnection()->prepare(self::$queries['globalInfo']);
            if (!$stmt->execute()){
                ErrorLogger::logFailedDBRequest($stmt->errorInfo(), $stmt->queryString,__LINE__, __FILE__);
                $message = 'Ошибка запроса информации об играх';
                throw new Exception($message);
            }
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (Throwable $e){
            ErrorLogger::logException($e);
            return null;
        }
    }
}

Авторизация


В настоящий момент для авторизации используется обычная сессия, куда записывается id зарегистрированного пользователя. В ближайшем будущем планирую посмотреть иные варианты готовых решений, но так как проект носит учебный характер пока везде использовал свои костыли.


Авторизация
class Authorization
{
    public static function check(){
        if (session_status() !== PHP_SESSION_ACTIVE){
            session_start();
        }
        return (isset($_SESSION['pl_id'])) ? true : false;
    }
    public static function logIn($playerId){
        if (!defined('LOGIN_SCRIPT') || LOGIN_SCRIPT !== '/login/server_scenarios/index.php') return false;

        if (session_status() !== PHP_SESSION_ACTIVE){
            session_start();
        }
        $_SESSION['pl_id'] = $playerId;
        return (isset($_SESSION['pl_id'])) ? true : false;
    }

    public static function logOut(){
        if (session_status() !== PHP_SESSION_ACTIVE){
            session_start();
        }
        unset($_SESSION['pl_id']);
        unset($_SESSION['cur_level']);
    }

    public static function getAuthorizedPlayerId(){
        if (session_status() !== PHP_SESSION_ACTIVE){
            session_start();
        }
        return isset($_SESSION['pl_id']) ? $_SESSION['pl_id'] : null;
    }
}

Игра "Слова из слова"


Кратко напомню правила: Необходимо составлять слова из показанного на экране слова. Слово должно быть нарицательным именем существительным в единственном числе. Уменьшительно-ласкательные формы, а также сокращения не принимаются. Минимальная длина слова — 3 буквы. Для перехода на следующий этап необходимо отгадать не менее 30% вариантов слов текущего.


Вид игрового поля

Word from word


Взаимодействие с сервером происходит посредством Ajax.


Скелет игрового поля до инициализации
<div class="row">
        <div class="col-xs-4 col-sm-3 player-info">
            <img src="/_app_files/players_avatars/no_avatar.png" alt="Ваш аватар" class="img-responsive img-circle center-block avatar" id="userAvatar">
            <h2 id="userLoginLabel">Игрок</h2>
            <p class="link-to-cabinet"><a href="/cabinet/">Вернуться в личный кабинет</a></p>

            <div class="tablescore">
                <div>Этап: <span id="level-number">0</span></div>
                <div>Очки: <span id="score-value">0</span></div>
            </div>

            <div class="progress">
                <span>Слов отгадано: <span id="found-words-number">0</span>/<span id="total-words-number">0</span></span>
                <div id="user-progress-bar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="0"> </div>
            </div>

            <div class="level-map-box">
                <div class="label label-info">Карта этапов <span class="glyphicon glyphicon-triangle-bottom"></span></div>
                <div class="btn-group-sm" id="level-buttons-container"></div>
            </div>

            <div class="tips">
                <div class="label label-info">Подсказки <span class="glyphicon glyphicon-triangle-bottom"></span></div>
                <div class="btn-group-sm">
                    <a class="btn">
                        <img id="word-definition-tip" title="Показать определение неотгаданного слова." alt="Показать определение неотгаданного слова." src="images/tips/definition_gray.png" draggable="false">
                    </a>
                    <a class="btn">
                        <img id="hole-word-tip" title="Показать неотгаданное слово целиком." alt="Показать неотгаданное слово целиком." src="images/tips/word_gray.png" draggable="false">
                    </a>
                </div>
            </div>

        </div>

        <div class="col-xs-8 col-sm-9 gamefield">
            <div id="missions-icon" class="row missions">

                <img id="mission1-icon" src="images/missions/incomplete.png" alt="Первая звезда" title="Отгадать больше 40% слов">
                <img id="mission2-icon" src="images/missions/incomplete.png" alt="Вторая звезда">
                <img id="mission3-icon" src="images/missions/incomplete.png" alt="Третья звезда" title="Отгадать 100% слов">
            </div>

            <div id="help-button"><a href="#help-box" data-toggle="modal"><span class="glyphicon glyphicon-question-sign"></span></a> </div>
            <!--Блок помощи-->
            <div id="help-box" class="modal fade">

                <!-- Модальное окно -->
                <div class="modal-dialog">

                    <!--Все содержимое модального окна -->
                    <div class="modal-content">

                        <!-- Заголовок модального окна -->
                        <div class="modal-header">
                            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">?</button>
                            <h4 class="modal-title">Помощь</h4>
                        </div>

                        <!-- Основное содержимое мод
                        ального окна -->
                        <div class="modal-body">
                            <!-- Текст правил -->
                        </div>

                    </div>
                    <!--Конец всего содержимого модального окна -->

                </div>
                <!--Конец модального окна -->

            </div>
            <!--Конец блока помощи-->

            <div id="user-input-word" class="row"></div>

            <div id="user-input-controls-btn" class="row">
                <div id="clear-letter-btn">Стереть букву</div>
                <div id="clear-word-btn">Стереть все слово</div>
            </div>

            <div id="level-main-word" class="row"></div>

            <div id="user-found-words-box" class="row"></div>
        </div>
    </div>
    <!--окно сообщений-->
    <div id="message-modal-box" class="modal fade">

        <!-- Модальное окно -->
        <div class="modal-dialog">

            <!--Все содержимое модального окна -->
            <div class="modal-content">
                <div class="modal-header">
                    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">?</button>
                    <h4 id="message-modal-header">Подсказка</h4>
                </div>

                <!-- Основное содержимое модального окна -->
                <div class="modal-body">

                    <p id="message-modal-content"></p>

                </div>

            </div>
            <!--Конец всего содержимого модального окна -->

        </div>
        <!--Конец модального окна -->

    </div>

Далее подгружаем данные о прогрессе пользователя.


Клиентский код инициализации
class Controller{
    private model: Model;
    private view: View;
    private freezeState: boolean = false;

    constructor(){
        this.view = new View();
        this.model = new Model(this.onReceiveInitialData.bind(this), this.onError.bind(this));

        $(document).on('tipClick',this.useTip.bind(this));
        $(document).on('lvlBtnClick',this.changeLevel.bind(this));
        $(document).on('letterClick', this.onLetterClick.bind(this));
        $(document).on('foundWordClick',this.getWordDefinition.bind(this));
        $(document).on('keydown', this.keyControls.bind(this));

        $('#clear-letter-btn').on('click', this.removeLastLetter.bind(this));
        $('#clear-word-btn').on('click', this.clearUserInput.bind(this));
    }
..........
}

class Model{
    readonly tipsCost = {
        holeWord: 250,
        wordDefinition: 100
    };
    private login: string;
    private avatar: string;
    private level: number;
    private totalLevelsNumber: number;
    private levelsPassedNumber: number;
    private levelWord: string;
    private wordVariants: Array<string>;
    private foundWords: Array<string>;
    private score: number;
    private userWord: string = '';
    private missions: {
        1: boolean,
        2: boolean,
        3: boolean
    };
    private missionUnique;
    private dictionary = {};

    constructor(success: ()=> any, error: (message: string)=>any, lvl?: number){
        let that = this;
        $.ajax({
            url: 'server_scenarios/index.php',
            type: 'post',
            data: {
                'action': 'getInitialInfo',
                'lvl' : (lvl) ? lvl : null
            },
            success: function (data) {
                if (data.state){
                    that.initialize(data);
                    success();
                } else {
                    error(data.message);
                }
            },
            error: function(){
                error('Ошибка соединения с сервером');
            }
        })
    }

    public initialize(data: ServerAnswerInitialData){
        this.login = data.login;
        this.avatar = data.avatar;
        this.level = parseInt(data.level);
        this.totalLevelsNumber = parseInt(data.totalLevelsNumber);
        this.levelsPassedNumber = parseInt(data.levelsPassedNumber);
        this.levelWord = data.levelWord;
        this.wordVariants = data.wordVariants;
        this.foundWords = data.foundWords;
        this.score = parseInt(data.score);
        this.missions = data.missions;
        this.missionUnique = data.missionUnique;
    }
........
}

class View{
    private playerInfo: PlayerInfo;
    private gamefield: Gamefield;
    public loader: Loader;
    constructor(){
        this.loader = new Loader();
        this.playerInfo = new PlayerInfo();
        this.gamefield = new Gamefield();
    }

    public initializePlayerInfoBox(data: UserInfoData){
        this.playerInfo.setNewAvatar(data.avatar);
        this.playerInfo.setLoginLabel(data.login);
        this.playerInfo.setLevelLabel(data.level.toString());
        this.playerInfo.setScoreLabel(data.score);
        this.playerInfo.setFoundWordsLabel(data.foundWordsNumber.toString());
        this.playerInfo.setTotalWordsLabel(data.totalWordsNumber.toString());
        this.playerInfo.setProgressBar(data.foundWordsNumber, data.totalWordsNumber);
        this.playerInfo.createLevelMap(data.totalLevelsNumber, data.level, data.levelsPassedNumber);
        if (data.tipsState.wordDefinition){
            this.playerInfo.enableTip('wordDefinition');
        }
        if (data.tipsState.holeWord){
            this.playerInfo.enableTip('holeWord');
        }
    }

    public initializeGameField(data: GamefieldData){
        for (let prop in data.missions){
            if (data.missions.hasOwnProperty(prop)){
                (data.missions[prop]) ? this.showCompleteMissionStateIcon(parseInt(prop)): this.showIncompleteMissionStateIcon(parseInt(prop));
            }
        }
        this.updateUserInputWord();
        this.gamefield.printMainWordLetters(data.levelMainWord);

        this.gamefield.clearFoundWordsBox();
        if (data.foundWords){
            for (let i = 0; i < data.foundWords.length; i++){
                this.addFoundWord(data.foundWords[i]);
            }
        }
        this.gamefield.setUniqueMissionTitle(data.missionUnique);
    }
....
}

Серверный код инициализации
if (!defined('PLAYER_ID') || !defined('CURRENT_LEVEL')) exit();
header('Content-Type: application/json');

$playerGlobalInfo = DBPlayerGlobalInfo::getGlobalInfo(PLAYER_ID);
$playerProgressInfo = DBPlayerProgress::getProgressOnLvl(PLAYER_ID, CURRENT_LEVEL);
$levelInfo = DBGameInfo::getLevelInfo(CURRENT_LEVEL);

echo json_encode(array(
    'state' => true,
    'login' => $playerGlobalInfo['login'],
    'avatar' => file_exists($_SERVER['DOCUMENT_ROOT'] . $playerGlobalInfo['avatar']) ? $playerGlobalInfo['avatar'] : '/_app_files/players_avatars/no_avatar.png',
    'level' => CURRENT_LEVEL,
    'totalLevelsNumber' => DBGameInfo::getLevelsQuantity(),
    'levelsPassedNumber' => DBPlayerProgress::getPassedLvlQuantity(PLAYER_ID),
    'levelWord' => $levelInfo['word'],
    'wordVariants' => $levelInfo['wordVariants'],
    'foundWords' => (empty($playerProgressInfo['foundWords'])) ? array() : $playerProgressInfo['foundWords'],
    'score' => DBPlayerProgress::getScore(PLAYER_ID),
    'missions' => array(
        1 => (boolean) $playerProgressInfo['star1status'],
        2 => (boolean) $playerProgressInfo['star2status'],
        3 => (boolean) $playerProgressInfo['star3status']
    ),
    'missionUnique' => $levelInfo['missionUnique']
));

Для того, чтобы избегать постоянных запросов к серверу и базе данных, в модели после инициализации хранятся найденные слова и варианты возможных слов для текущего этапа. В настоящее время они хранятся в открытом виде и доступны искушенному пользователю, который сможет залезть сначала в скрипт, а потом в консоль и увидеть все 100% слов. Поэтому буду рад, если кто подскажет в какую сторону копать, чтобы избежать этого варианта.


Проверка слова, вводимого игроком


Слово уровня представляет собой набор div'ов с одной буквой внутри. По нажатию на букву выполняется проверка активности буквы, если она не выбрана, то добавляется к набираемому слову и, как только длина слова станет больше либо равна, то оно отправляется на проверку наличия в словаре. Далее, когда проверка пройдена успешна, слова отправляется на сервер для подсчета очков и занесения в базу данных.
В случае, когда буква была выбрана, то проверяется ее положение в слове. Если крайняя — стирается, если нет — ничего не происходит.
Обработчик клика приведен ниже.


Обработчика клика по букве
onLetterClick(e: CustomEvent): void{
        if (this.freezeState) return;
        this.freeze();
        let letter = e.detail,
            userWord = this.model.getUserInputWord();
        if (!letter.hasClass('active')){
            userWord += letter.text();
            letter.data('order', userWord.length);
            this.view.setActiveLetterState(letter);
            this.view.updateUserInputWord(userWord);
            this.model.updateUserInputWord(userWord);
            if (userWord.length >= 3){
                this.model.checkUserWord(userWord, this.onAlreadyFoundWord.bind(this), this.onNewFoundWord.bind(this), );
            }
        } else {
            if (letter.data().order === userWord.length){
                letter.data('order', 0);
                userWord = userWord.substr(0, userWord.length-1);
                this.view.removeActiveLetterState(letter);
                this.view.updateUserInputWord(userWord);
                this.model.updateUserInputWord(userWord);
            }
        }
    }

Отправка найденного слова на сервер
checkUserWord(word: string, onAlreadyFound: (word) => any, onNewFound:(data: ServerAnswerCheckWord)=>any){
        let model = this;

        if (this.foundWords.indexOf(word) > -1){
            onAlreadyFound(word);
            return;
        }
        if (this.wordVariants.indexOf(word) > -1){
            $.ajax({
                url: 'server_scenarios/index.php',
                type: 'post',
                data: {
                    'action': 'checkWord',
                    'userWord' : word
                },
                success: function (data) {
                    if (data.state){
                        model.score = data.score;
                        model.foundWords = data.foundWords;
                        let level_status = false;
                        if ((data.lvl_status) && (model.totalLevelsNumber >= (model.level + 1))){
                            level_status = true;
                        }

                        onNewFound({
                            word: data.word,
                            score: data.score,
                            experience: data.experience,
                            points: data.points,
                            missions: data.missions,
                            foundWordsNumber: data.foundWords.length,
                            lvl_status: level_status
                        });
                    }
                }
            })
        }
    }

Серверный код обработки найденного слова
if (!defined('PLAYER_ID') || !defined('CURRENT_LEVEL') || empty($_POST['userWord'])) exit;
header('Content-Type: application/json');

$checker = new AddingWordChecker(PLAYER_ID,CURRENT_LEVEL,$_POST['userWord']);

echo json_encode(
    $checker->getChangedData()
);

class AddingWordChecker
{
    const POINTS_PER_LETTER = 4;
    const EXPERIENCE_PER_WORD = 1;

    const POINTS_FOR_LEVEL_COMPLETE = 150;
    const EXPERIENCE_FOR_LEVEL_COMPLETE = 20;

    const POINTS_FOR_FIRST_STAR = 1000;
    const EXPERIENCE_FOR_FIRST_STAR = 50;

    const POINTS_FOR_SECOND_STAR = 500;
    const EXPERIENCE_FOR_SECOND_STAR = 30;

    const POINTS_FOR_THIRD_STAR = 10000;
    const EXPERIENCE_FOR_THIRD_STAR = 250;

    const PERCENT_FOUND_FOR_LEVEL_COMPLETE = 0.3;
    const PERCENT_FOUND_FOR_FIRST_STAR = 0.4;
    const PERCENT_FOUND_FOR_THIRD_STAR = 1;

    private $playerId;
    private $state = false;
    private $gameLevel;
    private $levelStatus;
    private $wordToCheck;
    private $wordVariants;
    private $foundWords;
    private $star1status;
    private $star2status;
    private $star3status;
    private $changedData = array();

    function __construct($playerId, $gameLevel, $wordToCheck)
    {
        $wordToCheck = strip_tags($wordToCheck);

        $this->playerId = $playerId;
        $this->gameLevel = $gameLevel;
        $this->wordVariants = DBGameInfo::getWordVariantsOnLvl(CURRENT_LEVEL);

        $playerProgress = DBPlayerProgress::getProgressOnLvl($playerId, $gameLevel);
        $this->levelStatus = $playerProgress['lvl_status'];
        $this->foundWords = (empty($playerProgress['foundWords'])) ? array()  : $playerProgress['foundWords'];
        $this->star1status = $playerProgress['star1status'];
        $this->star2status = $playerProgress['star2status'];
        $this->star3status = $playerProgress['star3status'];

        $this->wordToCheck = $wordToCheck;

        if (!$this->checkWord()){
            $this->changedData['state']  = false;
            $this->changedData['message'] = 'Неверное слово';
            return;
        }

        $this->addWord();
    }

    public function getChangedData(){
        $this->changedData['state'] = $this->state;
        $this->changedData['score'] = DBPlayerProgress::getScore($this->playerId);
        $this->changedData['word'] = $this->wordToCheck;
        return $this->changedData;
    }

    private function checkWord(){
        if ((array_search($this->wordToCheck, $this->foundWords) !== false) ||
            (array_search($this->wordToCheck, $this->wordVariants) === false)) {
            $this->state = false;
        } else {
            $this->state = true;
        }
        return $this->state;
    }

    private function addWord(){
        if (!$this->state) return;

        array_push($this->foundWords, $this->wordToCheck);
        DBPlayerProgress::updateFoundWords($this->playerId, $this->gameLevel, $this->foundWords);
        $this->changedData['foundWords'] = $this->foundWords;

        $this->calculatePointsForWordLength();
        $this->checkLvlStatus();
        $this->checkMissions();
        DBPlayerProgress::augmentScore($this->playerId, $this->changedData['points']);
        DBPlayerGlobalInfo::augmentExperience($this->playerId, $this->changedData['experience']);
    }

    private function addPoints($points){
        if (isset($this->changedData['points'])){
            $this->changedData['points'] += $points;
        } else {
            $this->changedData['points'] = $points;
        }
    }

    private function addExperience($experience){
        if (isset($this->changedData['experience'])){
            $this->changedData['experience'] += $experience;
        } else {
            $this->changedData['experience'] = $experience;
        }
    }

    private function calculatePointsForWordLength(){
        $wordLength = mb_strlen($this->wordToCheck);
        $points = $wordLength * $this::POINTS_PER_LETTER;
        $experience = $this::EXPERIENCE_PER_WORD;
        switch ($wordLength){
            case (($wordLength > 3) && ($wordLength <= 5)):
                $points *= 1.1;
                $experience *= 2;
                break;
            case (($wordLength > 5) && ($wordLength <= 7)):
                $points *= 1.2;
                $experience *= 3;
                break;
            case (($wordLength > 7) && ($wordLength <= 9)):
                $points *= 1.3;
                $experience *= 4;
                break;
            case ($wordLength >= 10):
                $points *= 2;
                $experience *= 10;
                break;
        }

        $this->addPoints(floor($points));
        $this->addExperience(floor($experience));
    }

    private function checkLvlStatus(){
        if ((!$this->levelStatus) &&
            (count($this->foundWords) >= count($this->wordVariants)* $this::PERCENT_FOUND_FOR_LEVEL_COMPLETE)){
            $this->levelStatus = true;
            DBPlayerProgress::completeLevel($this->playerId, $this->gameLevel);
            $this->changedData['lvl_status'] = $this->levelStatus;

            $this->addPoints($this::POINTS_FOR_LEVEL_COMPLETE);
            $this->addExperience($this::EXPERIENCE_FOR_LEVEL_COMPLETE);
        }
    }

    private function checkMissions(){
        $this->changedData['missions'] = array();
        $this->checkFirstStarMission();
        $this->checkSecondStarMission();
        $this->checkThirdStarMission();
    }

    private function checkFirstStarMission(){
        if ($this->star1status) return;
        if (count($this->foundWords) >= count($this->wordVariants)* $this::PERCENT_FOUND_FOR_FIRST_STAR){
            $this->star1status = true;
            DBPlayerProgress::setCompleteStatusOnMission($this->playerId, $this->gameLevel, 1);
            $this->changedData['missions']['star1status'] = $this->star1status;

            $this->addPoints($this::POINTS_FOR_FIRST_STAR);
            $this->addExperience($this::EXPERIENCE_FOR_FIRST_STAR);
        }
    }

    private function checkSecondStarMission(){
        if ($this->star2status) return;
        $uniqueMission = DBGameInfo::getUniqueMission($this->gameLevel);
        $letter = array_keys($uniqueMission)[0];
        $quantity = array_values($uniqueMission)[0];
        $pattern = '/^'.$letter.'+/u';
        $matches = preg_grep($pattern, $this->foundWords);
        if (count($matches) >= $quantity){
            $this->star2status = true;
            DBPlayerProgress::setCompleteStatusOnMission($this->playerId, $this->gameLevel, 2);
            $this->changedData['missions']['star2status'] = $this->star2status;

            $this->addPoints($this::POINTS_FOR_SECOND_STAR);
            $this->addExperience($this::EXPERIENCE_FOR_SECOND_STAR);
        }
    }

    private function checkThirdStarMission(){
        if ($this->star3status) return;
        if (count($this->foundWords) >= count($this->wordVariants)* $this::PERCENT_FOUND_FOR_THIRD_STAR){
            $this->star3status = true;
            DBPlayerProgress::setCompleteStatusOnMission($this->playerId, $this->gameLevel, 3);
            $this->changedData['missions']['star3status'] = $this->star3status;

            $this->addPoints($this::POINTS_FOR_THIRD_STAR);
            $this->addExperience($this::EXPERIENCE_FOR_THIRD_STAR);
        }
    }
}

Заключение


Вот, собственно, и вкратце о моем проекте. Если кого-то заинтересуют подробности, с радостью отвечу. Буду благодарен за любые конструктивные замечания, а еще больше за совет, что и где нужно подучить, чтобы можно было превратить прототип платформы в полноценный работоспособный проект.


@Гуманитарий, который хочет стать технарем

Поделиться с друзьями
-->

Комментарии (0)