commit c55f0430317fb41b3270834a0e7e1edd37c95118 Author: MrBesen Date: Sun Feb 27 16:31:30 2022 +0100 first working diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06e5c77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +tictactoebot +*.a +*.o +*.ttf +*.png +*.jpg +*.conf +build/ +test +.vscode/settings.json \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0646b95 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "TAPI"] + path = TAPI + url = https://git.okaestne.de/okaestne/TAPI.git diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..e94fdbd --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,17 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/include/**", + "${workspaceFolder}/TAPI/include/**", + "${workspaceFolder}/TAPI/Log/**" + ], + "compilerPath": "/usr/bin/g++", + "cStandard": "c11", + "cppStandard": "c++17", + "intelliSenseMode": "gcc-x64" + } + ], + "version": 4 +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4f574d9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Debuggen (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/tictactoebot", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Automatische Strukturierung und Einrückung für \"gdb\" aktivieren", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "make all", + "miDebuggerPath": "/usr/bin/gdb" + }, + { + "name": "test Debuggen (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/test", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Automatische Strukturierung und Einrückung für \"gdb\" aktivieren", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "make test", + "miDebuggerPath": "/usr/bin/gdb" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..941a565 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "make all", + "type": "shell", + "command": "make -j all", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$gcc" + ] + }, + { + "label": "make clean", + "type": "shell", + "command": "make clean", + "problemMatcher": [] + }, + { + "label": "make test", + "type": "shell", + "command": "make -j test", + "problemMatcher": ["$gcc"], + } + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a058a9b --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ +# Author Yannis Gerlach +# Hochschule Osnabrück +# 13.11.2020 + +# `make clean all` nicht mit -j verwenden! -> race condition im make file +# statdessen: `make clean; make all -j` verwenden + +NAME = tictactoebot +NAMETEST = test +CFLAGS = -std=c++17 -O2 -g -pipe -Wall -Wextra -Wno-unused-parameter -Wpedantic -rdynamic #-march=native +CXX = g++ +SRCF = src/ +BUILDDIR = build/ +TESTF = tests/ +DEPF = $(BUILDDIR)deps/ +INCF = ./include/ +INCFS = $(shell find $(INCF) -type d) + +TAPIF = ./TAPI/ +TAPIA = $(TAPIF)libTAPI.a + +LOGF = $(TAPIF)Log/ +LOGO = $(LOGF)Log.o + +INCLUDES = -I$(LOGF) -I$(TAPIF)include/ $(addprefix -I, $(INCFS)) +LDFLAGS = -lcurl + +SRCFILES = $(shell find $(SRCF) -name "*.cpp") +OBJFILES = $(patsubst $(SRCF)%, $(BUILDDIR)%, $(patsubst %.cpp, %.o, $(SRCFILES))) $(LOGO) +DEPFILES = $(wildcard $(DEPF)*.d) + +SOURCEDIRS = $(shell find $(SRCF) -type d -printf "%p/\n") +BUILDDIRS = $(patsubst $(SRCF)%, $(BUILDDIR)%, $(SOURCEDIRS)) + +OBJFILESTEST = $(filter-out $(BUILDDIR)main.o, $(OBJFILES)) + +INCLUDES += $(addprefix -I, $(SOURCEDIRS)) + +all: $(NAME) runtest + +$(NAME): $(BUILDDIRS) $(DEPF) $(OBJFILES) $(TAPIA) + @echo "Linking $@" + @$(CXX) $(CFLAGS) -o $@ $(filter %.o, $^) $(filter %.a, $^) $(LDFLAGS) + +$(BUILDDIR)%.o: $(SRCF)%.cpp + @echo "Compiling: $@" + @$(CXX) $(CFLAGS) $(INCLUDES) $< -MM -MT $@ > $(DEPF)$(subst /,_,$*).d + @$(CXX) -c -o $@ $(CFLAGS) $(INCLUDES) $< + +$(NAME)_strip: $(NAME) + @echo "Strip $<" + @strip -o $@ $< + +%/: + mkdir -p $@ + +clean-depends: + $(RM) -r $(DEPF) + +$(LOGO): + $(MAKE) -C $(LOGF) all + +$(TAPIA): + $(MAKE) -C $(TAPIF) libTAPI.a + +clean: + $(RM) -r $(NAME) $(BUILDDIR) $(NAMETEST) $(NAME)_strip + $(MAKE) -C $(LOGF) $@ + $(MAKE) -C $(TAPIF) $@ + +$(NAMETEST): $(BUILDDIRS) $(DEPF) $(TESTF)*.cpp $(OBJFILESTEST) $(TAPIA) + @echo "Compiling tests" + @$(CXX) -o $@ $(filter %.o, $^) $(filter %.a, $^) $(filter %.cpp, $^) $(CFLAGS) -I$(SRCF) $(INCLUDES) $(LDFLAGS) + +runtest: $(NAMETEST) + @echo "Running tests" + ./$< + +.PHONY: clean all $(NAMETEST) clean-depends runtest + +include $(DEPFILES) diff --git a/TAPI b/TAPI new file mode 160000 index 0000000..7a7a384 --- /dev/null +++ b/TAPI @@ -0,0 +1 @@ +Subproject commit 7a7a3840da75f2287a2e7e7b858f71194c8a7b1f diff --git a/include/bot.h b/include/bot.h new file mode 100644 index 0000000..126f204 --- /dev/null +++ b/include/bot.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "config.h" +#include "TAPIManager.h" +#include "TAPIMarkup.h" + +#include "game.h" + +class Bot { +public: + Bot(TelegramAPI::Manager* tapi, Conf& conf); + ~Bot(); + + bool handleStart(TelegramAPI::Manager* api, const TelegramAPI::Message& msg); + + bool handleMessage(TelegramAPI::Manager* api, TelegramAPI::Message& msg); + bool handleCallback(TelegramAPI::Manager* api, TelegramAPI::CallbackQuery& clbq); + + void stop(); + +private: + TelegramAPI::Manager* tapi; + Conf& conf; + + void updateGame(int64_t chatid, uint64_t msgid, const Game* g); + TelegramAPI::InlineKeyboard gameToKeyboard(const Game*) const; + std::string gameToMessage(const Game*) const; + + // maps -> gameptr + std::map, Game*> games; +}; \ No newline at end of file diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..542044a --- /dev/null +++ b/include/config.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +struct Conf { + std::string token; + std::string apiurl; +}; \ No newline at end of file diff --git a/include/game.h b/include/game.h new file mode 100644 index 0000000..2a82690 --- /dev/null +++ b/include/game.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +class Game { +public: + enum class SYM : uint_fast8_t { + NONE = 0, + A = 1, + B = 2 + }; + + Game(); + + void setMessageID(uint32_t messageid); + + void setPlayerA(uint64_t player); + void setPlayerB(uint64_t player); + bool addPlayer(uint64_t player); + + uint64_t getPlayerA() const; + uint64_t getPlayerB() const; + + // true = next turn is playerA, false = next turn is playerB + constexpr bool getNextTurn() const { + return nextturna; + } + + // are both player set and the game is not over? + bool ready() const; + + // is the game over? (won / full) + bool done() 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); + + // 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; + constexpr uint_fast8_t getSize() const { return SIZE; } + +private: + static const uint_fast8_t SIZE = 3; + + const static std::string SYM_NAMES[3]; + + constexpr bool isInField(uint_fast8_t x, uint_fast8_t y) const { return (x < SIZE) && (y < SIZE); } + constexpr SYM getField(uint_fast8_t x, uint_fast8_t y) const { + return field[x + y * SIZE]; + } + constexpr void setField(uint_fast8_t x, uint_fast8_t y, SYM s) { + field[x + y * SIZE] = s; + } + constexpr bool isEmpty(uint_fast8_t x, uint_fast8_t y) const { + return field[x + y * SIZE] == SYM::NONE; + } + + SYM checkWinner() const; + + uint32_t messageid = 0; + uint64_t playerA = 0; + uint64_t playerB = 0; + + // true = next turn is playerA, false = next turn is playerB + bool nextturna = true; + + // fieldnr = x + (y * SIZE) + SYM field[SIZE * SIZE]; + +}; \ No newline at end of file diff --git a/src/bot.cpp b/src/bot.cpp new file mode 100644 index 0000000..b909918 --- /dev/null +++ b/src/bot.cpp @@ -0,0 +1,120 @@ +#include "bot.h" + +#include + +Bot::Bot(TelegramAPI::Manager* tapi, Conf& conf) : tapi(tapi), conf(conf) {} + +Bot::~Bot() { + for(auto it : games) { + delete it.second; + } +} + +bool Bot::handleStart(TelegramAPI::Manager* api, const TelegramAPI::Message& msg) { + if(msg.chat.id > 0) { + api->sendMessage(msg.chat.id, "This bot should be used in group chats"); + } + + + // create a 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); + + games.insert({{chatid, msgid}, g}); + + return true; + } + return false; +} + +bool Bot::handleMessage(TelegramAPI::Manager* api, TelegramAPI::Message& msg) { + 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()) { + api->answerCallbackQuery(clbq.id, "Invalid game"); + return true; + } + + Game* g = it->second; + if(data == "join") { + if(g->addPlayer(clbq.from.id)) { + api->answerCallbackQuery(clbq.id, "You joined the game"); + updateGame(chatid, msgid, g); + return true; + } + + api->answerCallbackQuery(clbq.id, "You can not join"); + return true; + } + + // try to parse "x y" + std::istringstream datastream(data); + 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)) { + api->answerCallbackQuery(clbq.id, "ok"); + updateGame(chatid, msgid, g); + return true; + } + api->answerCallbackQuery(clbq.id, "Error - this is not your turn"); + } + api->answerCallbackQuery(clbq.id, "Error - invalid button"); + + return true; +} + +void Bot::stop() { + +} + +void Bot::updateGame(int64_t chatid, uint64_t msgid, const Game* g) { + tapi->editMessageText(chatid, msgid, gameToMessage(g), gameToKeyboard(g)); +} + +TelegramAPI::InlineKeyboard Bot::gameToKeyboard(const Game* g) const { + static const std::string text[3] = {" ", "X", "O"}; + + TelegramAPI::InlineKeyboard kb; + for(uint_fast8_t row = 0; row < g->getSize(); row++) { + for(uint_fast8_t col = 0; col < g->getSize(); col++) { + Game::SYM s = g->getPos(col, row); + const std::string clb = std::to_string(col) + " " + std::to_string(row); + auto btn = TelegramAPI::InlineButton::createCallback(text[(int) s], clb); + kb.addButton(btn, row); + } + } + + if(!g->ready() && !g->done()) { + // missing player + kb.addButton(TelegramAPI::InlineButton::createCallback("Join Game", "join"), g->getSize()); + } + + return kb; +} + +std::string Bot::gameToMessage(const Game* g) const { + std::ostringstream out; + out << "X: " << g->getPlayerA() << std::endl; + out << "O: " << g->getPlayerB() << std::endl; + if(g->done()) { + out << "Winner: " << g->getWinner() << std::endl; + } else { + out << "nextturn: " << (g->getNextTurn() ? "X" : "O") << std::endl; + } + return out.str(); +} \ No newline at end of file diff --git a/src/game.cpp b/src/game.cpp new file mode 100644 index 0000000..be1de59 --- /dev/null +++ b/src/game.cpp @@ -0,0 +1,107 @@ +#include "game.h" +#include + +const std::string Game::SYM_NAMES[3] = {"NONE", "X", "O"}; + +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) { + playerA = player; +} +void Game::setPlayerB(uint64_t player) { + playerB = player; +} + +bool Game::addPlayer(uint64_t player) { + if(!playerA) { + playerA = player; + return true; + } + + if(!playerB && playerA != player) { + playerB = player; + return true; + } + return false; +} + +uint64_t Game::getPlayerA() const { + return playerA; +} + +uint64_t Game::getPlayerB() const { + return playerB; +} + +bool Game::ready() const { + return playerA != 0 && playerB != 0 && !done(); +} + +bool Game::done() const { + return checkWinner() != SYM::NONE; +} + +bool Game::turn(uint_fast8_t x, uint_fast8_t y, uint64_t player) { + if(!isInField(x, y)) return false; + if(!ready()) return false; + + // playerA + if(player == playerA && nextturna) { + SYM s = getField(x, y); + if(s != SYM::NONE) { + return false; + } + setField(x, y, SYM::A); + } + + //playerB + else if(player == playerB && !nextturna) { + SYM s = getField(x, y); + if(s != SYM::NONE) { + return false; + } + setField(x, y, SYM::B); + } + + // other user -> not allowed + else return false; + + nextturna = !nextturna; + return true; +} + +const std::string& Game::getWinner() const { + Game::SYM winner = checkWinner(); + if(winner == SYM::NONE) { + static const std::string EMPTY = ""; + return EMPTY; + } + return SYM_NAMES[(int) winner]; +} + +Game::SYM Game::getPos(uint_fast8_t x, uint_fast8_t y) const { + return getField(x, y); +} + +Game::SYM Game::checkWinner() const { + for(uint_fast8_t i = 0; i < SIZE; ++i) { + if(field[0 + i * SIZE] == field[1 + i * SIZE] && field[1 + i * SIZE] == field[2 + i * SIZE]) { + // spalte gleich + return field[0 + i * SIZE]; + } + if (field[i + 0 * SIZE] == field[i + 1 * SIZE] && field[i + 1 * SIZE] == field[i + 2 * SIZE]) { + // zeile gleich + return field[i + 0 * SIZE]; + } + } + return SYM::NONE; +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..5757a05 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include + +#include "TAPIInlineQuery.h" +#include "TAPIMarkup.h" +#include "TAPIManager.h" + +#include "Log.h" + +#include "bot.h" +#include "config.h" + +namespace tapi = TelegramAPI; + +class CommandStart : public TelegramAPI::Command { +public: + CommandStart(Bot& bot) : TelegramAPI::Command("/start", "Start the Bot"), bot(bot) {} + virtual bool process(TelegramAPI::Manager* m, const TelegramAPI::Message& msg) { + return bot.handleStart(m, msg); + } +private: + Bot& bot; +}; + +static bool run = true; + +void sig_handler(int sig_num) { + Log::info << "signalHandler triggered"; + run = false; + (void) sig_num; +} + +static bool loadConfig(Conf& out) { + std::ifstream conf("tictactoebot.conf"); + if(!conf) { + Log::error << "Could not open config file! tictactoebot.conf"; + return false; + } + while(conf && !conf.eof()) { + std::string key, value; + std::getline(conf, key, '='); + std::getline(conf, value); + + if(key == "token") { + out.token = value; + } else if(key == "apiurl") { + out.apiurl = value; + } else { + Log::info << "unused config value: \"" << key << "\": \"" << value << '"'; + } + } + conf.close(); + return true; +} + +int main(int argv, char** argc) { + Log::init(); + Log::setConsoleLogLevel(Log::Level::TRACE); + Log::setColoredOutput(true); + + Conf conf; + if(!loadConfig(conf)) return 1; + + TelegramAPI::Manager tmgr(conf.token); + + Bot bot(&tmgr, conf); + + Log::info << "Starting."; + + tmgr.registerCommand(std::make_unique(bot)); + namespace pl = std::placeholders; + tmgr.setMessageHandler(std::bind(&Bot::handleMessage, &bot, pl::_1, pl::_2)); + tmgr.setCallbackQueryHandler(std::bind(&Bot::handleCallback, &bot, pl::_1, pl::_2)); + + tmgr.setMyCommands(); + + signal(SIGINT, sig_handler); + signal(SIGTERM, sig_handler); + + while (run) { + tmgr.getUpdates(); + } + + bot.stop(); + + Log::stop(); + return 0; +} diff --git a/tests/main.cpp b/tests/main.cpp new file mode 100644 index 0000000..f55c453 --- /dev/null +++ b/tests/main.cpp @@ -0,0 +1,26 @@ +#include +#include "test.h" + +//tests + +test_t tests[] = {NULL}; + +int main(int argc, char** argv) { + + test_t* current = tests; + int failcount = 0; + int testcount = 0; + for(; *current; current++) { + testcount++; + printf("\033[1mRunning test number: %d ", testcount); + if((*current)()) { + printf("\033[1;92msucceeded\033[0;1m!\n"); + } else { + printf("\033[1;91mfailed\033[0;1m\n"); + failcount++; + } + } + + printf("\033[1;93m%d\033[0;1m/%d failed\n", failcount, testcount); + return failcount > 0; +} diff --git a/tests/test.h b/tests/test.h new file mode 100644 index 0000000..fb492f1 --- /dev/null +++ b/tests/test.h @@ -0,0 +1,12 @@ +#define TESTFAILED 0 +#define TESTGOOD 1 + +#include + +#define TESTDATA "./tests/data/" + +#define ASSERT(BED, ERR) if(!(BED)) { std::cout << __FILE__ << ":" << __LINE__ << " " << ERR << std::endl; return TESTFAILED; } +#define CMPASSERTE(A, B, ERR) if( !((A) == (B))) { std::cout << __FILE__ << ":" << __LINE__ << " is: \"" << (A) << "\" should: \"" << (B) << "\""<< std::endl; return TESTFAILED; } +#define CMPASSERT(A, B) CMPASSERTE(A, B, "") + +typedef int (*test_t)();