bot with mentions; recover gamestate from message

This commit is contained in:
mrbesen 2022-02-28 01:39:59 +01:00
parent abcfa64409
commit 36f27d8235
Signed by: MrBesen
GPG Key ID: 596B2350DCD67504
5 changed files with 210 additions and 63 deletions

2
TAPI

@ -1 +1 @@
Subproject commit 7a7a3840da75f2287a2e7e7b858f71194c8a7b1f
Subproject commit 1d99dc3e99afb192299e85804a2e6b194c480bf6

View File

@ -26,8 +26,7 @@ private:
void updateGame(int64_t chatid, uint64_t msgid, const Game* g);
TelegramAPI::InlineKeyboard gameToKeyboard(const Game*) const;
std::string gameToMessage(const Game*) const;
std::string gameToMessage(const Game*, std::vector<TelegramAPI::MessageEntity>& entites) const;
// maps <chatid, messageid> -> gameptr
std::map<std::pair<int64_t, uint64_t>, Game*> games;
Game* messageToGame(const TelegramAPI::Message& msg) const;
};

View File

@ -11,21 +11,40 @@ public:
B = 2
};
struct UserInfo {
uint64_t id = 0;
std::string name;
std::string username;
std::string getUsernameOrName() const;
constexpr operator bool() const {
return id != 0 || !username.empty();
}
bool operator==(const UserInfo& other) const;
constexpr bool operator!=(const UserInfo& other) const {
return other.id != id && other.username != username;
}
};
Game();
void setMessageID(uint32_t messageid);
void setPlayerA(const UserInfo& player);
void setPlayerB(const UserInfo& player);
bool addPlayer(const UserInfo& player);
void setPlayerA(uint64_t player);
void setPlayerB(uint64_t player);
bool addPlayer(uint64_t player);
uint64_t getPlayerA() const;
uint64_t getPlayerB() const;
const UserInfo& getPlayerA() const;
const UserInfo& getPlayerB() const;
// true = next turn is playerA, false = next turn is playerB
constexpr bool getNextTurn() const {
return nextturna;
}
constexpr void setNextTurn(bool isA) {
nextturna = isA;
}
// are both player set and the game is not over?
bool ready() const;
@ -33,14 +52,18 @@ public:
// is the game over? (won / full)
bool done() const;
// is atleast one space free?
bool isFull() const;
// do one turn
// returns false if the player doing this turn is not allowed or the game is done
bool turn(uint_fast8_t x, uint_fast8_t y, uint64_t player);
bool turn(uint_fast8_t x, uint_fast8_t y, const UserInfo& player);
// get the winner or empty string if game is not over yet
const std::string& getWinner() const;
SYM getPos(uint_fast8_t x, uint_fast8_t y) const;
void setPos(uint_fast8_t x, uint_fast8_t y, Game::SYM);
constexpr uint_fast8_t getSize() const { return SIZE; }
private:
@ -61,9 +84,8 @@ private:
SYM checkWinner() const;
uint32_t messageid = 0;
uint64_t playerA = 0;
uint64_t playerB = 0;
UserInfo playerA;
UserInfo playerB;
// true = next turn is playerA, false = next turn is playerB
bool nextturna = true;

View File

@ -4,11 +4,7 @@
Bot::Bot(TelegramAPI::Manager* tapi, Conf& conf) : tapi(tapi), conf(conf) {}
Bot::~Bot() {
for(auto it : games) {
delete it.second;
}
}
Bot::~Bot() {}
bool Bot::handleStart(TelegramAPI::Manager* api, const TelegramAPI::Message& msg) {
if(msg.chat.id > 0) {
@ -16,49 +12,50 @@ bool Bot::handleStart(TelegramAPI::Manager* api, const TelegramAPI::Message& msg
return true;
}
// create a game
// create a empty game
Game* g = new Game();
// g->addPlayer(msg.from.id); // does not work
auto sendmsg = api->sendMessage(msg.chat.id, gameToMessage(g), gameToKeyboard(g));
if(sendmsg) {
int64_t chatid = sendmsg.chat.id;
uint64_t msgid = sendmsg.messageId;
g->setMessageID(msgid);
std::vector<TelegramAPI::MessageEntity> entities;
const std::string text = gameToMessage(g, entities);
auto sendmsg = api->sendMessage(msg.chat.id, text, gameToKeyboard(g), entities);
delete g;
games.insert({{chatid, msgid}, g});
return true;
}
return false;
return true;
}
bool Bot::handleMessage(TelegramAPI::Manager* api, TelegramAPI::Message& msg) {
return true;
}
static Game::UserInfo userToUserInfo(const TelegramAPI::User& u) {
// + (u.last_name.empty() ? "" : " " + u.last_name) // problems with spaces and unicode in last name
return {u.id, u.first_name, u.username};
}
#define delret delete g; \
return true
bool Bot::handleCallback(TelegramAPI::Manager* api, TelegramAPI::CallbackQuery& clbq) {
int64_t chatid = clbq.message.chat.id;
uint64_t msgid = clbq.message.messageId;
const std::string& data = clbq.data;
auto it = games.find({chatid, msgid});
if(it == games.end()) {
Game* g = messageToGame(clbq.message);
if(!g) {
api->answerCallbackQuery(clbq.id, "Invalid game");
return true;
}
Game* g = it->second;
if(data == "join") {
if(g->addPlayer(clbq.from.id)) {
Game::UserInfo newuser = userToUserInfo(clbq.from);
if(g->addPlayer(newuser)) {
api->answerCallbackQuery(clbq.id, "You joined the game");
updateGame(chatid, msgid, g);
return true;
delret;
}
api->answerCallbackQuery(clbq.id, "You can not join");
return true;
delret;
}
// try to parse "x y"
@ -66,17 +63,22 @@ bool Bot::handleCallback(TelegramAPI::Manager* api, TelegramAPI::CallbackQuery&
int x = -1, y = -1;
datastream >> x >> y;
if(x != -1 && y != -1) {
Log::info << "turn: " << (int) x << " " << (int) y << " " << clbq.from.id;
if(g->turn(x, y, clbq.from.id)) {
Game::UserInfo player = userToUserInfo(clbq.from);
if(g->done()) {
api->answerCallbackQuery(clbq.id, "Error - game is over");
delret;
}
if(g->turn(x, y, player)) {
api->answerCallbackQuery(clbq.id, "ok");
updateGame(chatid, msgid, g);
return true;
delret;
}
api->answerCallbackQuery(clbq.id, "Error - this is not your turn");
api->answerCallbackQuery(clbq.id, "Error - this is not your turn");
delret;
}
api->answerCallbackQuery(clbq.id, "Error - invalid button");
return true;
delret;
}
void Bot::stop() {
@ -84,7 +86,9 @@ void Bot::stop() {
}
void Bot::updateGame(int64_t chatid, uint64_t msgid, const Game* g) {
tapi->editMessageText(chatid, msgid, gameToMessage(g), gameToKeyboard(g));
std::vector<TelegramAPI::MessageEntity> entities;
const std::string text = gameToMessage(g, entities);
tapi->editMessageText(chatid, msgid, text, gameToKeyboard(g), entities);
}
TelegramAPI::InlineKeyboard Bot::gameToKeyboard(const Game* g) const {
@ -108,14 +112,113 @@ TelegramAPI::InlineKeyboard Bot::gameToKeyboard(const Game* g) const {
return kb;
}
std::string Bot::gameToMessage(const Game* g) const {
static TelegramAPI::MessageEntity userInfoToEntity(const Game::UserInfo& userinfo) {
TelegramAPI::MessageEntity ent;
if(userinfo.username.empty()) {
// id bassed mention only works with users with no username
ent.type = TelegramAPI::MessageEntity::Type::TEXT_MENTION;
ent.user.id = userinfo.id;
ent.length = userinfo.name.size();
} else {
ent.type = TelegramAPI::MessageEntity::Type::MENTION;
ent.length = userinfo.username.size()+1;
}
return ent;
}
std::string Bot::gameToMessage(const Game* g, std::vector<TelegramAPI::MessageEntity>& entites) const {
std::ostringstream out;
out << "X: " << g->getPlayerA() << std::endl;
out << "O: " << g->getPlayerB() << std::endl;
out << "X: " << g->getPlayerA().getUsernameOrName() << std::endl;
out << "O: " << g->getPlayerB().getUsernameOrName() << std::endl;
if(g->done()) {
out << "Winner: " << g->getWinner() << std::endl;
if(g->getWinner().empty()) {
out << "Draw" << std::endl;
} else {
out << "Winner: " << g->getWinner() << std::endl;
}
} else {
out << "nextturn: " << (g->getNextTurn() ? "X" : "O") << std::endl;
}
//create entities
if(g->getPlayerA()) {
TelegramAPI::MessageEntity a = userInfoToEntity(g->getPlayerA());
a.offset = 3;
entites.push_back(a);
if(g->getPlayerB()) {
TelegramAPI::MessageEntity b = userInfoToEntity(g->getPlayerB());
b.offset = 7 + a.length; // ("x: " + "\n" + "O: ").size() = 7
entites.push_back(b);
}
}
return out.str();
}
static Game::UserInfo userinfoFromEntity(const TelegramAPI::MessageEntity& ent, const std::string& messageText) {
if(ent.type == TelegramAPI::MessageEntity::Type::TEXT_MENTION) {
uint64_t userid = ent.user.id;
std::string name = ent.user.first_name;
std::string username = ent.user.username;
return {userid, name, username};
}
if(ent.type == TelegramAPI::MessageEntity::Type::MENTION) {
return {0, "", messageText.substr(ent.offset+1, ent.length-1)}; // without the "@"
}
// invlaid user entity
return {0, "", ""};
}
Game* Bot::messageToGame(const TelegramAPI::Message& msg) const {
if(msg.entities.size() > 2) {
Log::warn << "To many entities: " << msg.entities.size();
return nullptr;
}
if(msg.entities.size() == 0) {
// no user (empty game)
return new Game();
}
Game* g = new Game();
// read the userinfo from the entities
for(uint_fast8_t i = 0; i < msg.entities.size(); ++i) {
const TelegramAPI::MessageEntity& ent = msg.entities.at(i);
Game::UserInfo info = userinfoFromEntity(ent, msg.text);
if(!info) {
// invalid
Log::warn << "invalid entity " << (int) i;
delete g;
return nullptr;
}
g->addPlayer(info);
}
// read the next turn value
char nextturn = msg.text.at(msg.text.size()-1);
g->setNextTurn(nextturn == 'X');
// get the game
for(uint_fast8_t i = 0; i < msg.replyMarkup.size(); ++i) {
const std::vector<TelegramAPI::MarkupButton>& row = msg.replyMarkup.at(i);
for(uint_fast8_t j = 0; j < row.size(); ++j) {
const TelegramAPI::MarkupButton& btn = row.at(j);
// button is part of game
if(i < g->getSize()) {
Game::SYM s = Game::SYM::NONE;
if(btn.text == "X") {
s = Game::SYM::A;
} else if(btn.text == "O") {
s = Game::SYM::B;
}
g->setPos(j, i, s);
}
}
}
return g;
}

View File

@ -3,25 +3,31 @@
const std::string Game::SYM_NAMES[3] = {"NONE", "X", "O"};
std::string Game::UserInfo::getUsernameOrName() const {
return username.empty() ? name : ("@" + username);
}
bool Game::UserInfo::operator==(const UserInfo& other) const {
if(id != 0 && other.id != 0) {
return id == other.id;
}
return username == other.username;
}
Game::Game() {
for(uint_fast8_t i = 0; i < SIZE * SIZE; ++i) {
field[i] = SYM::NONE;
}
}
void Game::setMessageID(uint32_t messageid) {
this->messageid = messageid;
}
void Game::setPlayerA(uint64_t player) {
void Game::setPlayerA(const UserInfo& player) {
playerA = player;
}
void Game::setPlayerB(uint64_t player) {
void Game::setPlayerB(const UserInfo& player) {
playerB = player;
}
bool Game::addPlayer(uint64_t player) {
bool Game::addPlayer(const UserInfo& player) {
if(!playerA) {
playerA = player;
return true;
@ -34,26 +40,39 @@ bool Game::addPlayer(uint64_t player) {
return false;
}
uint64_t Game::getPlayerA() const {
const Game::UserInfo& Game::getPlayerA() const {
return playerA;
}
uint64_t Game::getPlayerB() const {
const Game::UserInfo& Game::getPlayerB() const {
return playerB;
}
bool Game::ready() const {
return playerA != 0 && playerB != 0 && !done();
return playerA && playerB && !done();
}
bool Game::done() const {
return checkWinner() != SYM::NONE;
return (checkWinner() != SYM::NONE) || isFull();
}
bool Game::turn(uint_fast8_t x, uint_fast8_t y, uint64_t player) {
bool Game::isFull() const {
for(uint_fast8_t i = 0; i < SIZE * SIZE; ++i) {
if(field[i] == SYM::NONE)
return false;
}
return true;
}
bool Game::turn(uint_fast8_t x, uint_fast8_t y, const UserInfo& player) {
if(!isInField(x, y)) return false;
if(!ready()) return false;
Log::info << "turn player: " << player.id << " n: " << player.name << " u: " << player.username;
Log::info << "playerA: " << playerA.id << " n: " << playerA.name << " u: " << playerA.username << " e: " << (player == playerA);
Log::info << "playerB: " << playerB.id << " n: " << playerB.name << " u: " << playerB.username << " e: " << (player == playerB);
// playerA
if(player == playerA && nextturna) {
SYM s = getField(x, y);
@ -92,6 +111,10 @@ Game::SYM Game::getPos(uint_fast8_t x, uint_fast8_t y) const {
return getField(x, y);
}
void Game::setPos(uint_fast8_t x, uint_fast8_t y, Game::SYM s) {
setField(x, y, s);
}
static constexpr bool checkThree(Game::SYM s1, Game::SYM s2, Game::SYM s3) {
return s1 == s2 && s2 == s3;
}