Привет, Хабр! Сегодня поговорим об игре «Угадай слово» и её вариациях, например Wordle («Вордли»). Скорее всего, вы с ней сталкивались, а если нет, поясню: она похожа на старую добрую «Виселицу» — есть загаданное слово, и надо за ограниченное количество попыток его угадать. Только отгадываем слово не по буквам, а целиком.
В какой-то момент игра стала так популярна, что некоторые компании интегрировали её в разные маркетинговые акции внутри приложений. Казалось бы, тема закрыта, можно расходиться. Как бы не так! Мне есть чем вас удивить, ведь сегодня займёмся разработкой именно такой игры, геймплей которой базируется на SMS-сообщениях. Подробности — под катом.
![](https://habrastorage.org/getpro/habr/upload_files/07a/600/c3e/07a600c3e8c85a4c424c05a0e8ce99a5.jpg)
Отмечу, что благодаря тестовому периоду MTС Exolve можно попробовать себя в создании аналога Wordly. В нашей версии можно загадать определённое слово для кого-то из ваших друзей или близких.
И даже если у вас нет мобильного телефона или нулевой баланс счёта, статья может быть полезной. Ведь прежде чем дойти до SMS, напишем скрипт с HTTP API для игры. Поэтому сыграть в нашу «угадайку» можно и без SMS (и регистрации) с помощью привычных инструментов для запросов к API — например, cURL или Postman.
Записывайте рецепт. Для реализации понадобится:
любой хостинг с php и SQlite (можно бесплатный)
оформить бесплатный тестовый период в MTС Exolve и подтвердить номер
PHP-скрипт c API и скрипт обработки SMS
Что за «Угадайка» такая?
Если вдруг ажиотаж вокруг Wordle и его аналогов прошёл мимо вас, то объясню механику игры:
есть загаданное слово — существительное из 5 букв
слово, которое совершенно неизвестно игроку
игрок вводит предполагаемое слово
ему подсвечиваются буквы. В примере ниже:
○ серый — буквы нет в слове
○ жёлтый — буква есть в слове, но стоит не на своём месте
○ зелёный — буква указана вернотакже, как правило, нам для удобства подсвечены буквы, которые уже вводились
игрок должен за 6 попыток отгадать слово
![Пример среднестатистического интерфейса для игры в аналоги Wordly Пример среднестатистического интерфейса для игры в аналоги Wordly](https://habrastorage.org/getpro/habr/upload_files/c87/cdd/87e/c87cdd87efefb5343c4c632baf057bc0.png)
Поскольку разрабатываем SMS-игру, то у нас есть некоторые ограничения на отображение информации, в том числе на количество символов в сообщении. Поэтому не будем полностью копировать механику игры. В конце концов это было бы неинтересно.
В нашей игре будут кое-какие отличия от оригинала:
Предоставляем возможность задать слово произвольной длины, через настройки, или просто возьмём случайное слово из словаря русских существительных (я брал словарь здесь).
Количество попыток равно длине слова.
Буквы, которые стоят не на своём месте в слове, подсвечивать не получится. Поэтому их записываем отдельным сообщением.
«Клавиатуры» с уже набранными буквами нет, их вполне заменит история SMS-переписки.
Скрин с демонстрацией игры я размещу в конце статьи. Ну а теперь, когда все точки над «і» расставлены, пришла пора браться за код.
Пишем API
Дисклеймер. Я не программист, поэтому не стоит принимать решения в этой статье за эталон кода, в процессе разработки я руководствовался принципом «главное, чтобы работало». Поэтому предложенный механизм с API вряд ли соответствует большинству хороших практик проектирования. Вы всегда можете его самостоятельно доработать. Совершенству нет предела.
Весь исходный код проекта размещён на GitHub.
Ну а пока давайте разбираться со структурой нашего сервиса.
Методы API
Логика API реализуется внутри скрипта api.php.
API будет использовать 3 метода:
POST — для создания новой игры
PUT — для ввода предполагаемого слова
GET — для получения информации о текущем прогрессе
База данных
Данные будем хранить в базе данных SQLite3 (db.sqlite).
В таблице games будет информация о текущей игре.
В таблице words разместим словарь русских существительных.
Структура БД — на рисунке ниже:
![](https://habrastorage.org/getpro/habr/upload_files/b40/1b2/6b7/b401b26b7f6a6b0893574ce5ee8bcf4f.png)
В таблице words всего один столбец со словами из словаря, что, в принципе, очевидно.
А вот для таблицы games расписать поля полезно:
id — первичный ключ (ID записи)
phone — телефон, для которого начата игра
hidden_world — загаданное слово
current_world — текущее состояние отгаданного слова, с указанием верно подставленных букв
last_try_word — последнее отправленное слово, используется как защита от случайной повторной отправки того же самого слова (чтобы не списывать за это попытку)
in_word — список букв, которые есть в слове, но стоят не на своём месте
attempts — счётчик количества попыток
Вспомогательные файлы
Ещё наш API использует файл настроек game-config.json. В нём можно в формате «ключ-значение» загадать для телефона определённое слово.
Если номера нет в настройках, то слово будет взято из словаря.
"phones_words": {
"79121100234": "дом",
"79111977999": "дерево"
},
Ещё к вспомогательным файлам можно отнести файл логов api_log.txt. В логи пишутся телефон, слово (если есть), метод API и метка времени.
Пример: 79111977999 пелёнка POST 2023-09-12T00:07:53+03:00
Диаграмма последовательности
Давайте на диаграмме последовательности разберём подробно сценарий, в котором создаём новую игру, получаем информацию о текущем статусе и отправляем один раз слово на проверку.
![](https://habrastorage.org/getpro/habr/upload_files/38a/434/9a9/38a4349a9d421bc7d3a476de37d42571.png)
Для упрощения схемы я опущу процесс чтения настроек из файла, записи логов и обработки исключений, сосредоточимся непосредственно на логике игры.
В этот раз вместо сценария предлагаю перейти к коду.
Код API
Полностью код файла api.php можно посмотреть под спойлером.
Код
<?php
// Guess the word game API (c) BosonBeard 2023
// Headers
header("Access-Control-Allow-Orgin: *");
header("Access-Control-Allow-Methods: *");
header("Content-Type: text/plain");
// Get parameters from query
$method = $_SERVER['REQUEST_METHOD'];
$phone = $_REQUEST["phone"];
$user_word = mb_strtolower($_REQUEST["word"]);
// write request in log
$dt = date('c', time()); // get currtent time and date
$fw = fopen("api_log.txt", "a+");
fwrite($fw, $phone . " " . $user_word . " " . $method . " " . $dt . "\r\n");
fclose($fw);
//connect to DB
$db = new SQLite3('db.sqlite');
if ($phone) {
switch ($method) {
// GET method - game status info
case "GET":
// get last key command
$sql = "SELECT `hidden_word`,`current_word`,`in_word`,`attempts` FROM games WHERE `phone` = '$phone' ";
//$sql = "SELECT `hidden_word`,`current_word`,`in_word` FROM games WHERE `phone` = '$phone' ORDER BY `timestamp` DESC";
$result = $db->querySingle($sql, true);
$hidden_word = mb_strtolower($result['hidden_word']);
$current_word = mb_strtolower($result['current_word']);
$attempts = $result['attempts'];
$have_attempts = mb_strlen($hidden_word) - $attempts;
if ($result['in_word']) {
$in_word = $result['in_word'] . " — не на своём месте \n";
}
// set API response
if ($result) {
if ($current_word != $hidden_word) {
$response =
"Ваше слово: $result[current_word] \n" .
"Осталось попыток: $have_attempts \n"
. $in_word;
} else {
$response =
"Вы угадали! \n" .
"Загаданное слово: $hidden_word \n" .
"Ещё оставалось попыток: $have_attempts \n";
}
} else {
$response = "Начните новую игру";
}
break;
// POST method - start new game
case "POST":
// get params from game-config.json
$file = "game-config.json";
$json = json_decode(file_get_contents($file), true);
$hidden_word = mb_strtolower($json['phones_words'][$phone]);
if (!$hidden_word) {
$numRows = $db->querySingle("SELECT COUNT(*) as count FROM words");
// echo "$numRows=".$numRows;
$hidden_word = mb_strtolower($db->querySingle("SELECT * FROM words LIMIT 1 OFFSET " . (rand(0, $numRows))));
}
$current_word = str_repeat("_", mb_strlen($hidden_word));
$have_attempts = mb_strlen($hidden_word);
// add new not processed command in DB
$sql = "UPDATE games
SET `hidden_word`='$hidden_word', `current_word`='$current_word',
`in_word`='', `attempts`=0, `last_try_word` ='' WHERE `phone` = $phone";
$result = $db->querySingle($sql);
$sql = "INSERT INTO games (`phone`, `hidden_word`, `current_word`,`in_word`, `last_try_word`)
VALUES('$phone','$hidden_word', '$current_word', '', '')";
$result = $db->querySingle($sql);
// set API response
$response =
"Новая игра \n" .
"Слово: $current_word \n" .
"Букв: " . mb_strlen($current_word) . " \n";
"Осталось попыток: $have_attempts \n";
break;
// PUT method - try to guess word
case "PUT":
$sql = "SELECT `hidden_word`,`current_word`, `last_try_word`, `in_word`, `attempts` FROM games WHERE `phone` = '$phone' ";
$result = $db->querySingle($sql, true);
$current_word = mb_strtolower($result['current_word']);
$hidden_word = mb_strtolower($result['hidden_word']);
$last_try_word = mb_strtolower($result['last_try_word']);
$attempts = $result['attempts'];
$have_attempts = mb_strlen($hidden_word) - ($attempts + 1);
// if DB is empty for this number, crate new game
if (!$result) {
$response = "Начните новую игру";
} else {
// check guessed word for noun. Reading dictionary from DB
$is_noun = $db->querySingle("SELECT `word` FROM words WHERE `word` = '$user_word' ");
if (!$is_noun) {
// if not noun try another word
$response =
"Введённое слово не является существительным в именительном падеже: \n" .
"Повторите попытку. \n";
}
// check to true guess
else {
// if guess - true
if ($user_word == $hidden_word) {
//fixes a bug with the counter increasing when guessing
if ($hidden_word != $current_word) {
$attempts += 1;
}
$have_attempts = mb_strlen($hidden_word) - ($attempts);
$sql = " UPDATE games
SET `hidden_word`='$hidden_word', `current_word`='$user_word',
`in_word`='', `attempts`= $attempts WHERE `phone` = $phone";
$result = $db->querySingle($sql);
$response =
"Вы угадали!\n" .
"Загаданное слово: $hidden_word \n" .
"Ещё оставалось попыток: $have_attempts \n";
}
// if the user sends the same word, again
elseif ($user_word == $last_try_word) {
$have_attempts += 1;
$response =
"В прошлый раз вы уже вводили\n" .
"слово: $last_try_word \n" .
"Ещё осталось попыток: $have_attempts \n";
}
// user word not the same length to hidden word
else {
if (mb_strlen($hidden_word) != mb_strlen($user_word)) {
$response =
"Длина вашего слова не совпадает с загаданным! \n" .
"Повторите попытку.\n";
"Текущий прогресс: $current_word \n";
}
else {
// if user don't have attempts any more
if ($have_attempts === 0) {
$response =
"Ваше слово: $user_word \n" .
"Загаданное слово: $hidden_word \n" .
"Попытки закончились, начните новую игру \n";
}
// if guess word is false
else {
$check_result = check_correct_letters($hidden_word, $user_word, $current_word);
$attempts += 1;
$sql = " UPDATE games
SET `hidden_word`='$hidden_word', `current_word`='$check_result->current_word',
`in_word`='$check_result->in_word', `attempts`= $attempts, `last_try_word`= '$user_word'
WHERE `phone` = $phone";
$result = $db->querySingle($sql);
if ($check_result->in_word) {
$in_word = $check_result->in_word . " — не на своём месте \n";
}
$response =
"Ваше слово: $check_result->current_word \n" .
"Осталось попыток:" . ($have_attempts) . " \n"
.$in_word;
}
}
}
}
}
break;
// any other HTTP method
default:
$response = '{"error":"unknown method"}';
break;
}
}
// phone is empty
else {
$response = '{"error":"не передан телефон"}';
}
//return API response
echo $response;
// function for getting correct letters in word, and get correct letters on wrong places in word
function check_correct_letters($hidden_word, $user_word, $current_word)
{
// get unicode array from string
$hidden_word_arr = preg_split('//u', $hidden_word, 0, PREG_SPLIT_NO_EMPTY);
$user_word_arr = preg_split('//u', $user_word, 0, PREG_SPLIT_NO_EMPTY);
$current_word_arr = preg_split('//u', $current_word, 0, PREG_SPLIT_NO_EMPTY); // true letters on right place in word
$in_word_arr = array(); // true letter on wrong place in word
for ($i = 0; $i < count($user_word_arr); $i++) {
for ($j = 0; $j < count($hidden_word_arr); $j++) {
if ($hidden_word_arr[$j] == $user_word_arr[$i]) {
// get true letter on right place
if ($j == $i) {
if ($current_word_arr[$j] == "_") {
$current_word_arr[$i] = $user_word_arr[$i];
}
}
// get true letter on wrong place in word
else {
array_push($in_word_arr, $user_word_arr[$i]);
}
}
}
}
$in_word_arr = array_unique($in_word_arr, SORT_REGULAR);
return (object) [
'current_word' => implode($current_word_arr),
'in_word' => implode(",", $in_word_arr),
];
}
Давайте разберём основные блоки кода:
// Headers
header("Access-Control-Allow-Orgin: *");
header("Access-Control-Allow-Methods: *");
header("Content-Type: text/plain");
// Get parameters from query
$method = $_SERVER['REQUEST_METHOD'];
$phone = $_REQUEST["phone"];
$user_word = mb_strtolower($_REQUEST["word"]);
// write request in log
$dt = date('c', time()); // get currtent time and date
$fw = fopen("api_log.txt", "a+");
fwrite($fw, $phone . " " . $user_word . " " . $method . " " . $dt . "\r\n");
fclose($fw);
//connect to DB
$db = new SQLite3('db.sqlite');
if ($phone) {
Стартовый блок.
Устанавливаем заголовки для страницы.
Затем считываем query-параметры. Чтобы упростить код, я решил даже для метода POST не использовать передачу параметров в body. Поэтому все параметры всегда передаются в query.
Далее добавляется запись в лог с текущей датой и временем. Потом подключаемся к SQLite-базе данных и в конце делаем проверку, указан ли в запросе телефон (если не указан, возвращается ошибка).
Потом — основная логика трёх методов API.
Начинаем с метода GET.
switch ($method) {
// GET method - game status info
case "GET":
// get last key command
$sql = "SELECT `hidden_word`,`current_word`,`in_word`,`attempts` FROM games WHERE `phone` = '$phone' ";
//$sql = "SELECT `hidden_word`,`current_word`,`in_word` FROM games WHERE `phone` = '$phone' ORDER BY `timestamp` DESC";
$result = $db->querySingle($sql, true);
$hidden_word = mb_strtolower($result['hidden_word']);
$current_word = mb_strtolower($result['current_word']);
$attempts = $result['attempts'];
$have_attempts = mb_strlen($hidden_word) - $attempts;
if ($result['in_word']) {
$in_word = $result['in_word'] . " — не на своём месте \n";
}
// set API response
if ($result) {
if ($current_word != $hidden_word) {
$response =
"Ваше слово: $result[current_word] \n" .
"Осталось попыток: $have_attempts \n"
. $in_word;
} else {
$response =
"Вы угадали! \n" .
"Загаданное слово: $hidden_word \n" .
"Ещё оставалось попыток: $have_attempts \n";
}
} else {
$response = "Начните новую игру";
}
break;
Как было показано на схеме выше, метод возвращает информацию о текущем ходе игры. Поэтому просто делаем SELECT-запрос в базу данных. Если в ответе БД столбец in_word был не пустой, записываем его значение в переменную по заданному шаблону сообщения, чтобы затем вернуть его пользователю.
Далее проверяем, есть ли вообще запись в БД (если нет, предложим начать новую игру). Если база данных нам что-то вернула, проверяем, угадал ли игрок слово ранее. В зависимости от этого возвращаем сообщение.
Следующий метод — POST.
case "POST":
// get params from game-config.json
$file = "game-config.json";
$json = json_decode(file_get_contents($file), true);
$hidden_word = mb_strtolower($json['phones_words'][$phone]);
if (!$hidden_word) {
$numRows = $db->querySingle("SELECT COUNT(*) as count FROM words");
// echo "$numRows=".$numRows;
$hidden_word = mb_strtolower($db->querySingle("SELECT * FROM words LIMIT 1 OFFSET " . (rand(0, $numRows))));
}
$current_word = str_repeat("_", mb_strlen($hidden_word));
$have_attempts = mb_strlen($hidden_word);
// add new not processed command in DB
$sql = "UPDATE games
SET `hidden_word`='$hidden_word', `current_word`='$current_word',
`in_word`='', `attempts`=0, `last_try_word` ='' WHERE `phone` = $phone";
$result = $db->querySingle($sql);
$sql = "INSERT INTO games (`phone`, `hidden_word`, `current_word`,`in_word`, `last_try_word`)
VALUES('$phone','$hidden_word', '$current_word', '', '')";
$result = $db->querySingle($sql);
// set API response
$response =
"Новая игра \n" .
"Слово: $current_word \n" .
"Букв: " . mb_strlen($current_word) . " \n";
"Осталось попыток: $have_attempts \n";
break;
В данном методе сначала читаем настройки из файла game-config.json. Для API нам надо получить привязку загаданного слова к номеру телефона. Если в настройках нет привязки, то вытаскиваем случайное слово из таблицы words БД.
Теперь необходимо создать в БД запись о новой игре. У нас для одного номера телефона создаётся только одна запись. Поскольку у меня на хостинге была старая версия SQLite, я не смог использовать UPSERT-логику, пришлось делить на вызов вначале UPDATE, а затем INSERT.
Если всё прошло успешно, возвращаем пользователю загаданное слово, количество букв в нём и количество оставшихся попыток.
Остался самый большой метод — PUT.
case "PUT":
$sql = "SELECT `hidden_word`,`current_word`, `last_try_word`, `in_word`, `attempts` FROM games WHERE `phone` = '$phone' ";
$result = $db->querySingle($sql, true);
$current_word = mb_strtolower($result['current_word']);
$hidden_word = mb_strtolower($result['hidden_word']);
$last_try_word = mb_strtolower($result['last_try_word']);
$attempts = $result['attempts'];
$have_attempts = mb_strlen($hidden_word) - ($attempts + 1);
// if DB is empty for this number, crate new game
if (!$result) {
$response = "Начните новую игру";
} else {
// check guessed word for noun. Reading dictionary from DB
$is_noun = $db->querySingle("SELECT `word` FROM words WHERE `word` = '$user_word' ");
if (!$is_noun) {
// if not noun try another word
$response =
"Введённое слово не является существительным в именительном падеже: \n" .
"Повторите попытку. \n";
}
// check to true guess
else {
// if guess - true
if ($user_word == $hidden_word) {
//fixes a bug with the counter increasing when guessing
if ($hidden_word != $current_word) {
$attempts += 1;
}
$have_attempts = mb_strlen($hidden_word) - ($attempts);
$sql = " UPDATE games
SET `hidden_word`='$hidden_word', `current_word`='$user_word',
`in_word`='', `attempts`= $attempts WHERE `phone` = $phone";
$result = $db->querySingle($sql);
$response =
"Вы угадали!\n" .
"Загаданное слово: $hidden_word \n" .
"Ещё оставалось попыток: $have_attempts \n";
}
// if the user sends the same word, again
elseif ($user_word == $last_try_word) {
$have_attempts += 1;
$response =
"В прошлый раз вы уже водили\n" .
"слово: $last_try_word \n" .
"Ещё осталось попыток: $have_attempts \n";
}
// user word not the same length to hidden word
else {
if (mb_strlen($hidden_word) != mb_strlen($user_word)) {
$response =
"Длина вашего слова не совпадает с загаданным! \n" .
"Повторите попытку.\n";
"Текущий прогресс: $current_word \n";
}
else {
// if user don't have attempts any more
if ($have_attempts === 0) {
$response =
"Ваше слово: $user_word \n" .
"Загаданное слово: $hidden_word \n" .
"Попытки закончились, начните новую игру \n";
}
// if guess word is false
else {
$check_result = check_correct_letters($hidden_word, $user_word, $current_word);
$attempts += 1;
$sql = " UPDATE games
SET `hidden_word`='$hidden_word', `current_word`='$check_result->current_word',
`in_word`='$check_result->in_word', `attempts`= $attempts, `last_try_word`= '$user_word'
WHERE `phone` = $phone";
$result = $db->querySingle($sql);
if ($check_result->in_word) {
$in_word = $check_result->in_word . " — не на своём месте \n";
}
$response =
"Ваше слово: $check_result->current_word \n" .
"Осталось попыток:" . ($have_attempts) . " \n"
.$in_word;
}
}
}
}
}
break;
В начале считываем из БД текущий прогресс.
Вычисляем новое количество оставшихся попыток.
Затем начинаются проверки:
Проверка на наличие записи в БД. Если записи нет, предложим начать новую игру.
Проверка, угадал ли игрок слово. Если угадал, обновим данные в БД, чтобы потом метод GET вернул информацию о победе. После чего пишем в переменную $response сообщение о победе. Если не угадал, идём дальше по цепочке проверок.
Проверяем, не отправил ли пользователь то же слово, что и в прошлый раз. Если отправил, то счётчик попыток не увеличиваем и сообщаем пользователю о повторе.
Проверяем, совпадает ли длина загаданного и отправленного слова. Если нет, сообщаем об этом пользователю, счётчик попыток не увеличиваем.
Проверяем, не закончились ли попытки.
После всех проверок вызывается функция check_correct_letters (описание будет ниже), записываем в базу текущий прогресс и возвращаем пользователю результат проверки.
В любом случае в конце всех кейсов пользователю возвращается ответ, ранее записанный в переменную $response.
default:
$response = '{"error":"unknown method"}';
break;
}
}
// phone is empty
else {
$response = '{"error":"не передан телефон"}';
}
echo $response;
Осталось только разобрать функцию для проверки букв в слове.
function check_correct_letters($hidden_word, $user_word, $current_word)
{
// get unicode array from string
$hidden_word_arr = preg_split('//u', $hidden_word, 0, PREG_SPLIT_NO_EMPTY);
$user_word_arr = preg_split('//u', $user_word, 0, PREG_SPLIT_NO_EMPTY);
$current_word_arr = preg_split('//u', $current_word, 0, PREG_SPLIT_NO_EMPTY); // true letters on right place in word
$in_word_arr = array(); // true letter on wrong place in word
for ($i = 0; $i < count($user_word_arr); $i++) {
for ($j = 0; $j < count($hidden_word_arr); $j++) {
if ($hidden_word_arr[$j] == $user_word_arr[$i]) {
// get true letter on right place
if ($j == $i) {
if ($current_word_arr[$j] == "_") {
$current_word_arr[$i] = $user_word_arr[$i];
}
}
// get true letter on wrong place in word
else {
array_push($in_word_arr, $user_word_arr[$i]);
}
}
}
}
$in_word_arr = array_unique($in_word_arr, SORT_REGULAR);
return (object) [
'current_word' => implode($current_word_arr),
'in_word' => implode(",", $in_word_arr),
];
}
Передаём в функцию загаданное слово, отправленное пользователем слово и текущее слово в базе. Для начала разбиваем все строки на массивы букв. Затем создаём пустой массив букв, которые стоят в слове не на своих местах. Для каждого нового запроса это будет новый список.
Логика проверки незамысловатая. Существует основной цикл, в котором проходимся по каждому символу загаданного слова.
Если символы совпали, то проверяем, совпали ли их позиции в слове. Если совпали, значит буква угадана правильно, надо обновить текущее слово (current_word). Если позиции не совпали, значит буква есть в слове, но на другом месте, поэтому пишем её в in_word_arr.
Алгоритм неидеальный и иногда даёт небольшие сбои, но для нашей бесплатной игры вполне подойдёт. API готово, теперь можно протестировать его с помощью Postman.
![](https://habrastorage.org/getpro/habr/upload_files/8df/03b/d8d/8df03bd8dc8c76ab5d17f2f7ecd7d70d.png)
Пишем скрипт для обработки SMS
Теперь, когда вся логика игры реализована, подключаю обработку и отправку SMS-сообщений с помощью платформы МТС Exolve. В прошлой статье я уже писал, как зарегистрироваться и получить бесплатный номер телефона с тестовым балансом в 300 рублей, поэтому повторяться не буду. Но теперь нужно сделать дополнительный шаг в настройках личного кабинета и указать адрес нашего скрипта listener.php для обработки SMS. Перейдите в раздел «Настройки» и укажите URL до скрипта listener.php, размещённого на вашем хостинге.
![](https://habrastorage.org/getpro/habr/upload_files/fdd/28d/af7/fdd28daf73b153b689ad603b026f804e.png)
После этого каждое пришедшее или отправленное SMS будет вызывать наш скрипт listener.php.
Второй скрипт будет выполнять достаточно простую задачу — получать уведомления об SMS-сообщениях. Обрабатывать входящие сообщения в зависимости от текста, вызывать методы API и пересылать результат работы API в ответном SMS.
Всего будет 4 сценария для входящих SMS-сообщений, и все они представлены на диаграмме ниже:
![](https://habrastorage.org/getpro/habr/upload_files/204/393/5d5/2043935d5be516ef96e0e5e26e154d5f.png)
Перейдём к коду. Полный код файла listener.php — под спойлером.
Код
<?php
// MTS Exolve SMS listener for guess the word game API
const PAGE_ENCODING = 'UTF-8';
// Heaers
header("Access-Control-Allow-Orgin: *");
header("Access-Control-Allow-Methods: *");
header("Content-Type: application/json");
$data = json_decode(file_get_contents('php://input'), true);
// Get parameters from SMS
//$method = $_SERVER['REQUEST_METHOD'];
$phone = $data["sender"];
$receiver = $data["receiver"];
$text = trim(mb_strtolower($data["text"]));
$direction = strtolower($data["direction"]);
$billing_status = strtolower($data["billing_status"]);
// listen olly incoming messages
// only incoming and billed SMS (prevent double send)
if ($direction == strtolower("DIRECTION_INCOMING") and $billing_status == strtolower("BILLING_STATUS_BILLED") ) {
// get params from game-config.json
$file = "game-config.json";
$json = json_decode(file_get_contents($file), true);
$sms_api_key = $json['config']['sms_api_key'];
$sms_api_url = $json['config']['sms_api_url'];
$url = $json['config']['base_url'];
//we get phone parameter for call api here
$dt = date('c', time()); // get currtent time and date
// write request in log
$fw = fopen("listener_log.txt", "a+");
fwrite($fw, $phone . " " . $text . " " . $dt . "\r\n");
fclose($fw);
if ($phone) {
// empty SMS or text = help - send hint
if ($text == "" or $text == "help") {
$response =
"Игра «Угадай слово», отправьте: \n" .
"start - новая игра \n" .
"info - текущий статус игры \n" .
"help или пусто - это сообщение \n" .
"любое другое слово - угадываемое слово \n";
$res = send_SMS($phone, $receiver, $response, $sms_api_key, $sms_api_url);
}
// start new game (use POST method game API)
elseif ($text == "start") {
$res = call_API($url, "POST", $phone, $text);
$response = $res; // get text message from game api
$res2 = send_SMS($phone, $receiver, $response, $sms_api_key, $sms_api_url);
}
// send game status (GET method of game API)
elseif ($text == "info") {
$res = call_API($url, "GET", $phone, $text);
$response = $res; // get text message from game api
$res2 = send_SMS($phone, $receiver, $response, $sms_api_key, $sms_api_url);
}
// send word for checking (PUT method of game API)
else {
$res = call_API($url, "PUT", $phone, $text);
$response = $res; // get text message from game api
$res2 = send_SMS($phone, $receiver, $response, $sms_api_key, $sms_api_url);
}
echo $response;
}
}
else {
echo "error not incoming message";
}
// call game api (api.php)
function call_API($api_url, $method, $user_phone, $user_word)
{
// setup curl
$curl = curl_init();
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
));
switch ($method) {
case "PUT":
curl_setopt($curl, CURLOPT_PUT, 1);
$url = $api_url . "?phone=" . $user_phone . "&word=" . $user_word;
curl_setopt($curl, CURLOPT_URL, $url);
break;
case "POST":
curl_setopt($curl, CURLOPT_POST, 1);
$url = $api_url . "?phone=" . $user_phone;
curl_setopt($curl, CURLOPT_URL, $url);
break;
case "GET":
$url = $api_url . "?phone=" . $user_phone;
curl_setopt($curl, CURLOPT_URL, $url);
break;
default:
$response = '{"error":"unknown method"}';
break;
}
$result = curl_exec($curl);
if (!$response) {
$response = $result;
}
curl_close($curl);
return $response;
}
// call MTS Exolve SMS send API
function send_SMS($phone, $receiver, $text, $sms_api_key, $sms_api_url)
{
// create POST body
$data = array("destination" => $phone, "number" => $receiver, "text" => $text);
$data_json = json_encode($data);
// setup curl
$curl = curl_init();
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 0);
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Authorization: Bearer ' . $sms_api_key
));
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_URL, $sms_api_url);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data_json);
$result = curl_exec($curl);
sleep(1);
curl_close($curl);
return $result;
}
Давайте разберём блоки кода. Но поскольку тут логика во многом аналогична API, я опущу часть подробностей.
$data = json_decode(file_get_contents('php://input'), true);
// Get parameters from SMS
//$method = $_SERVER['REQUEST_METHOD'];
$phone = $data["sender"];
$receiver = $data["receiver"];
$text = trim(mb_strtolower($data["text"]));
$direction = strtolower($data["direction"]);
$billing_status = strtolower($data["billing_status"]);
// listen olly incoming messages
// only incoming and billed SMS (prevent double send)
if ($direction == strtolower("DIRECTION_INCOMING") and $billing_status == strtolower("BILLING_STATUS_BILLED") ) {
В данном блоке получаем тело уведомления от МТС Exolve. Подробнее с форматом уведомлений можно ознакомиться в документации.
Далее фильтруем только доставленные входящие сообщения и читаем вторую часть из файла настроек (объект config).
$file = "game-config.json";
$json = json_decode(file_get_contents($file), true);
$sms_api_key = $json['config']['sms_api_key'];
$sms_api_url = $json['config']['sms_api_url'];
$url = $json['config']['base_url'];
Содержимое файла настроек:
"config": {
"base_url": "ваш домен, на котором лежит скрипт/guess_word/api.php",
"sms_api_key":"Token приложения см. в ЛК MTС Exolve",
"sms_api_url":"https://api.exolve.ru/messaging/v1/SendSMS – адрес метода Exolve API"
}
Затем в зависимости от текста сообщения:
просто присылаем в ответном сообщении подсказку по игре (help или пусто)
в остальных случаях вызываем методы api.php и возвращаем пользователю SMS с ответом
if ($text == "" or $text == "help") {
$response =
"Игра «Угадай слово», отправьте: \n" .
"start - новая игра \n" .
"info - текущий статус игры \n" .
"help или пусто - это сообщение \n" .
"любое другое слово - угадываемое слово \n";
$res = send_SMS($phone, $receiver, $response, $sms_api_key, $sms_api_url);
}
// start new game (use POST method game API)
elseif ($text == "start") {
$res = call_API($url, "POST", $phone, $text);
$response = $res; // get text message from game api
$res2 = send_SMS($phone, $receiver, $response, $sms_api_key, $sms_api_url);
}
// send game status (GET method of game API)
elseif ($text == "info") {
$res = call_API($url, "GET", $phone, $text);
$response = $res; // get text message from game api
$res2 = send_SMS($phone, $receiver, $response, $sms_api_key, $sms_api_url);
}
// send word for checking (PUT method of game API)
else {
$res = call_API($url, "PUT", $phone, $text);
$response = $res; // get text message from game api
$res2 = send_SMS($phone, $receiver, $response, $sms_api_key, $sms_api_url);
}
echo $response;
Функцию call_API подробно разбирать не будем — там просто вызов API с разными параметрами с помощью cURL. Тем более, что она по логике очень похожа на следующую функцию send_SMS.
function send_SMS($phone, $receiver, $text, $sms_api_key, $sms_api_url)
{
// create POST body
$data = array("destination" => $phone, "number" => $receiver, "text" => $text);
$data_json = json_encode($data);
// setup curl
$curl = curl_init();
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 0);
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Authorization: Bearer ' . $sms_api_key
));
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_URL, $sms_api_url);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data_json);
$result = curl_exec($curl);
sleep(1);
curl_close($curl);
return $result;
}
В данной функции настраиваются параметры отправки SMS-сообщения пользователю с помощью МТС Exolve. Параметры простые: кому отправить, с какого номера отправить и текст сообщения. Подробнее можно посмотреть в документации.
Затем отправляем POST-запрос к API для отправки SMS-сообщений.
Безудержно развлекаемся
Когда всё готово, остаётся лишь одно — играть. Если не задавать слово заранее в настройках, то игра получается достаточно сложной, потому что в словаре очень много длинных и неожиданных слов. Но так даже лучше. Как и обещал, плоды наших трудов — на скриншотах ниже.
![](https://habrastorage.org/getpro/habr/upload_files/951/dab/60d/951dab60d23462c699d70d461c6a4eac.png)
Единственный недостаток: в тестовом режиме SMS-сообщения отправляются только на номер, который привязан к личному кабинету. Поэтому если вы хотите сделать подарок-сюрприз кому-то в виде SMS-игры с каким-то своим необычным загаданным словом, рекомендую перевести аккаунт из тестового режима в полноценный.
Надеюсь, статья вызвала у вас интерес, буду рад конструктивным комментариям.